preface
The public account “Java Programming Notes” record Java learning daily, share learning road dribs and drabs, from entry to give up, welcome to pay attention to
We have already put a simple Spring Security Demo project up and running, but using the default User username and default automatically generated password, this article will add DB based permission authentication more suitable for production environment. The overall implementation is divided into two parts
- DB – based permission table design
- Spring Security authentication extension point implementation
DB – based permission table design
RBAC is introduced
RBAC is role-based Access Control. In THE setting of RBAC, users are bound to roles and roles are bound to permissions. A user can have multiple roles and a Role can have multiple permissions.
The following is the classic table structure design, user table, role table, permission table, user role table, role permission table
The users table
CREATE TABLE `user` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT 'id',' username 'varchar(10NOT NULL DEFAULT '' COMMENT ', 'password' varchar(255) NOT NULL DEFAULT '' COMMENT ', 'name' varchar(20NOT NULL DEFAULT '' COMMENT ', 'email' varchar(36) NOT NULL COMMENT 'email ',' phone 'varchar(20) DEFAULT NULL COMMENT '手机号',
`sex` tinyint(2) NOT NULL DEFAULT '0'COMMENT' gender ', 'age' tinyint(2) DEFAULT '0'COMMENT' age ', 'user_type' tinyint(2) NOT NULL DEFAULT '1'COMMENT' user category [0: administrator,1: Regular employees]', 'locked' tinyint(2) DEFAULT '0Whether 'COMMENT' is locked [0: normal,1', 'status' tinyint(3) NOT NULL DEFAULT '1'COMMENT' status [0: failure,1: normal]', 'create_time' NOT NULL DEFAULT '1970- 01- 01 00:00:00'COMMENT' update_time 'datetime NOT NULL DEFAULT'1970- 01- 01 00:00:00Mysql > alter table InnoDB AUTO_INCREMENT= InnoDB AUTO_INCREMENT= InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4;
Copy the code
Character sheet
CREATE TABLE `role` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT 'primary key id',
`name` varchar(64) NOT NULL COMMENT 'Role name',
`description` varchar(255) DEFAULT NULL COMMENT 'introduction',
`icon_cls` varchar(32) DEFAULT NULL COMMENT 'Character icon',
`seq` tinyint(2) NOT NULL DEFAULT '0' COMMENT 'Sort number',
`status` tinyint(2) NOT NULL DEFAULT '1' COMMENT 'Status [0: invalid,1: normal]',
`create_time` datetime NOT NULL DEFAULT '1970-01-01 00:00:00' COMMENT 'Creation time',
`update_time` datetime NOT NULL DEFAULT '1970-01-01 00:00:00' COMMENT 'Update Time'.PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=9 DEFAULT CHARSET=utf8 COMMENT=The 'role';
Copy the code
User role table
CREATE TABLE `user_role` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT 'primary key id',
`user_id` int(11) NOT NULL COMMENT 'user id',
`role_id` int(11) NOT NULL COMMENT 'character id'.PRIMARY KEY (`id`),
KEY `idx_user_role_ids` (`user_id`,`role_id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=79 DEFAULT CHARSET=utf8 COMMENT='User roles';
Copy the code
Permissions on the table
CREATE TABLE `resource` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT 'primary key',
`name` varchar(64) NOT NULL COMMENT 'Resource Name',
`permissions` varchar(32) DEFAULT NULL COMMENT 'Resource Permissions',
`url` varchar(100) DEFAULT NULL COMMENT 'Resource path',
`open_mode` varchar(32) DEFAULT NULL COMMENT '打开方式 ajax,iframe',
`description` varchar(255) DEFAULT NULL COMMENT 'Resource Introduction',
`icon_cls` varchar(32) DEFAULT NULL COMMENT 'Resource Icon',
`pid` int(11) DEFAULT NULL COMMENT 'Parent resource ID',
`seq` tinyint(2) NOT NULL DEFAULT '0' COMMENT 'order',
`status` tinyint(2) NOT NULL DEFAULT '1' COMMENT 'Status [0: invalid,1: normal]',
`opened` tinyint(1) NOT NULL DEFAULT '0' COMMENT 'Open state',
`resource_type` tinyint(2) NOT NULL DEFAULT '0' COMMENT 'Resource Type',
`create_time` datetime NOT NULL DEFAULT '1970-01-01 00:00:00' COMMENT 'Creation time',
`update_time` datetime NOT NULL DEFAULT '1970-01-01 00:00:00' COMMENT 'Update Time'.PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=239 DEFAULT CHARSET=utf8 COMMENT='resources';
Copy the code
Role permission table
CREATE TABLE `role_resource` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT 'primary key id',
`role_id` int(11) NOT NULL COMMENT 'character id',
`resource_id` int(11) NOT NULL COMMENT 'resource id'.PRIMARY KEY (`id`),
KEY `idx_role_resource_ids` (`role_id`,`resource_id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=683 DEFAULT CHARSET=utf8 COMMENT='Character Resources';
Copy the code
Import the above SQL into DB
Mybatis – Plus introduction
Mybatis. Plus/guide/insta…
MyBatis-Plus (Opens New Window) (MP) is a new enhancement tool for MyBatis (Opens New Window), which is designed to simplify development and improve efficiency.
vision
Our vision is to become the best partner of MyBatis, just like 1P and 2P in Contra, the efficiency of gay friends is doubled.
Add mybatis-plus SpringBoot && Mysql driver dependency
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.4.2</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.38</version>
</dependency>
Copy the code
application.yml
configuration
Fill in your OWN DB information here
# DataSource Config
spring:
datasource:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://localhost:3306/security? useUnicode=true&useSSL=false&characterEncoding=utf8
username: root
password: 123456
Copy the code
Automatic code generation
addmybatis-plus-generator
Dependency to automatically generate code
There is a problem with the freemarker package that comes with mybatis plus-Generator and a new version (2.3.28) needs to be introduced for it to work properly
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-generator</artifactId>
<version>3.4.2</version>
</dependency>
<dependency>
<groupId>org.freemarker</groupId>
<artifactId>freemarker</artifactId>
<version>2.3.28</version>
<scope>compile</scope>
</dependency>
Copy the code
Use Mybatis – Plus provide Demo, we automatically generate table of the Controller, the Service, the DAO, Mapper file
/** * * read the console contents *
*/
public static String scanner(String tip) {
Scanner scanner = new Scanner(System.in);
StringBuilder help = new StringBuilder();
help.append("Please enter" + tip + ":");
System.out.println(help.toString());
if (scanner.hasNext()) {
String ipt = scanner.next();
if (StringUtils.isNotBlank(ipt)) {
returnipt; }}throw new MybatisPlusException("Please enter the correct one" + tip + "!");
}
public static void main(String[] args) {
// Code generator
AutoGenerator mpg = new AutoGenerator();
// Global configuration
GlobalConfig gc = new GlobalConfig();
String projectPath = System.getProperty("user.dir");
gc.setOutputDir(projectPath + "/src/main/java");
gc.setAuthor("uiaoo");
gc.setOpen(false);
// gc.setSwagger2(true); Entity attribute Swagger2 annotation
mpg.setGlobalConfig(gc);
// Data source configuration
DataSourceConfig dsc = new DataSourceConfig();
dsc.setUrl("jdbc:mysql://localhost:3306/security? useUnicode=true&useSSL=false&characterEncoding=utf8");
// dsc.setSchemaName("public");
dsc.setDriverName("com.mysql.jdbc.Driver");
dsc.setUsername("root");
dsc.setPassword("123456");
mpg.setDataSource(dsc);
/ / package configuration
PackageConfig pc = new PackageConfig();
pc.setModuleName(scanner("Module name"));
pc.setParent("com.uiaoo.spring.security");
mpg.setPackageInfo(pc);
// Custom configuration
InjectionConfig cfg = new InjectionConfig() {
@Override
public void initMap(a) {
// to do nothing}};// If the template engine is freemarker
String templatePath = "/templates/mapper.xml.ftl";
// Customize the output configuration
List<FileOutConfig> focList = new ArrayList<>();
// Custom configurations are printed first
focList.add(new FileOutConfig(templatePath) {
@Override
public String outputFile(TableInfo tableInfo) {
// Customize the output file name. If your Entity has a prefix or suffix, note that the XML name will change accordingly!!
return projectPath + "/src/main/resources/mapper/" + pc.getModuleName()
+ "/" + tableInfo.getEntityName() + "Mapper"+ StringPool.DOT_XML; }}); cfg.setFileOutConfigList(focList); mpg.setCfg(cfg);// Configure the template
TemplateConfig templateConfig = new TemplateConfig();
templateConfig.setXml(null);
mpg.setTemplate(templateConfig);
// Policy configuration
StrategyConfig strategy = new StrategyConfig();
strategy.setNaming(NamingStrategy.underline_to_camel);
strategy.setColumnNaming(NamingStrategy.underline_to_camel);
strategy.setEntityLombokModel(true);
strategy.setRestControllerStyle(true);
strategy.setInclude(scanner("Table name, separated by multiple Commas").split(","));
strategy.setControllerMappingHyphenStyle(true);
strategy.setTablePrefix(pc.getModuleName() + "_");
mpg.setStrategy(strategy);
mpg.setTemplateEngine(new FreemarkerTemplateEngine());
mpg.execute();
}
Copy the code
The auto-generated directory looks like this and contains most of the regular code files
Spring Security authentication extension point implementation
SpringSecurityFilterChain
Spring Security in the web application of scene core implementation for the Bean name for this Bean SpringSecurityFilterChain, Class is org. Springframework. Security. Web. Named FilterChainProxy, SpringSecurityFilterChain in internal maintains a FilterChain, By default, the following filters are maintained in FilterChain
UsernamePasswordAuthenticationFilter
We will follow-up meaning to explain in detail the realization of each Filter function, here we focus on SpringSecurityFilterChain under the Filter implementation, the name can be roughly guess is associated with the login account password Filter, UsernamePasswordAuthenticationFilter inherited from AbstractAuthenticationProcessingFilter doFilter method after the execution will enter the attemptAuthentication this method, namely try certification, a point to note here is that Authentication using the implementation class is UsernamePasswordAuthenticationToken, In the subsequent AuthenticationProvider supports method will match to the realization of DaoAuthenticationProvider
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
if (this.postOnly && ! request.getMethod().equals("POST")) {
throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
} else {
String username = this.obtainUsername(request); username = username ! =null ? username : "";
username = username.trim();
String password = this.obtainPassword(request); password = password ! =null ? password : "";
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
this.setDetails(request, authRequest);
return this.getAuthenticationManager().authenticate(authRequest); }}Copy the code
AuthenticationManager
Method is finally enclosing getAuthenticationManager (.) authenticate (authRequest), namely AuthenticationManager# authenticate method, The AuthenticationManager class abstracts the authentication model. As can be seen from the description of the Authenticate method, it tries to pass authentication and returns a result data filled with user information and authentication information.
ProviderManager
Spring Security provides the implementation class ProviderManager of AuthenticationManager by default. In the implementation of The Authenticate method of ProviderManager, ProviderManager envisages a variety of authentication options, such as regular account password authentication, tripartite authentication, and so on, primarily traversing all the AuthenticationProvider implementations, The provider. Supports method is used to identify whether the current passed authentication object implementation is supported by the current provider. If it is not supported, the authentication object implementation is skipped until a matching one is found
Class<? extends Authentication> toTest = authentication.getClass();
// Get all AuthenticationProvider implementations, loop through, if supports, authenticate, otherwise next Provider
for (AuthenticationProvider provider : getProviders()) {
if(! provider.supports(toTest)) {continue; }...try {
result = provider.authenticate(authentication);
if(result ! =null) {... }}catch() {... }}Copy the code
AuthenticationProvider
The Authenticate method supports is defined in the AuthenticationProvider method
- Supports current authentication matches the current Provider, remember above
UsernamePasswordAuthenticationFilter
theauthentication
The implementation of theUsernamePasswordAuthenticationToken
Right? This is going to match by default toDaoAuthenticationProvider
.DaoAuthenticationProvider
It’s not implemented by itselfsupports
Method, the real implementation isAbstractUserDetailsAuthenticationProvider
And theAbstractUserDetailsAuthenticationProvider
The implementation ofDaoAuthenticationProvider
, so it matches by defaultDaoAuthenticationProvider
- Authenticate Indicates the actual authentication method
The core of the default AuthenticationProvider AbstractUserDetailsAuthenticationProvider achieved most of the common key authenticate and supports method, logic method It also provides an extended abstract method retrieveUser, which is called to retrieveUser information when no user information is retrieved from the cache (NullUserCache is also null by default cache implementation). DaoAuthenticationProvider retrieveUser method is achieved,
public abstract class AbstractUserDetailsAuthenticationProvider
implements AuthenticationProvider.InitializingBean.MessageSourceAware {
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {... String username = determineUsername(authentication);boolean cacheWasUsed = true;
// Retrieve user information from the cache
UserDetails user = this.userCache.getUserFromCache(username);
if (user == null) {
cacheWasUsed = false;
try {
// Query user information
user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication);
}
catch (UsernameNotFoundException ex) {
this.logger.debug("Failed to find user '" + username + "'");
if (!this.hideUserNotFoundExceptions) {
throw ex;
}
throw new BadCredentialsException(this.messages
.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials"."Bad credentials")); }... }}/ / the implementation of the authentication UsernamePasswordAuthenticationToken
@Override
public boolean supports(Class
authentication) {
return(UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication)); }}Copy the code
In DaoAuthenticationProvider implementation, the emergence of a new service UserDetailsService, UserDetailsService is the core of a user information service interface, loadUserByUsername only one method, Through userName query, the encapsulated user information UserDetails object is returned. The analysis can finally be concluded here. Although Spring Security also provides default implementation such as JdbcUserDetailsManager, the overall flexibility is not enough. This is where you can implement your own UserDetailsService
public class DaoAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider {
@Override
protected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication)
throws AuthenticationException {
prepareTimingAttackProtection();
try {
/ / call UserDetailsService. LoadUserByUsername get user information
UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
if (loadedUser == null) {
throw new InternalAuthenticationServiceException(
"UserDetailsService returned null, which is an interface contract violation");
}
return loadedUser;
}
catch (UsernameNotFoundException ex) {
mitigateAgainstTimingAttack(authentication);
throw ex;
}
catch (InternalAuthenticationServiceException ex) {
throw ex;
}
catch (Exception ex) {
throw newInternalAuthenticationServiceException(ex.getMessage(), ex); }}}Copy the code
That’s a little bit too much. Let me draw a picture to make sense of it
implementation
implementationAuthenticationProvider
Here we inherited directly implement DaoAuthenticationProvider classes, do nothing, direct use of DaoAuthenticationProvider original authenticate method
public class MyAuthenticationProvider extends DaoAuthenticationProvider {
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
return super.authenticate(authentication); }}Copy the code
implementationUserDetailsService
@Slf4j
@Component
public class MyUserDetailsService implements UserDetailsService {
@Autowired
private IUserService iUserService;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// Check whether the user exists
User userInfo = iUserService.getAdminByUserName(username);
if(Objects.isNull(userInfo)){
throw new UsernameNotFoundException("User does not exist");
}
// Query permission information based on the user nameList<Resource> resourceList = iUserService.getResourcesByUserName(username); List<SimpleGrantedAuthority> authList = resourceList.stream().filter(v-> ! StringUtils.isEmpty(v.getPermissions())).map(v -> new SimpleGrantedAuthority(v.getPermissions())).collect(Collectors.toList());// {noop} does not use password encryption
User user = new User(username,"{noop}"+userInfo.getPassword(),authList);
log.info("user info : {}",user); return user; }}Copy the code
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper.User> implements IUserService {
@Override
public List<Resource> getResourcesByUserName(String userName) {
// Query basic user information
User user = getAdminByUserName(userName);
if(Objects.isNull(user)){
return new ArrayList<>();
}
// Query the role associated with the user
List<UserRole> tAdminRoleList = iUserRoleService.getRolesByUserId(user.getId());
List<Integer> roleIds = new ArrayList<>();
tAdminRoleList.forEach(tAdminRole -> {
roleIds.add(tAdminRole.getRoleId());
});
// Query associated permission information based on the role ID
returniRoleResourceService.getResource(roleIds); }}Copy the code
Implement WebSecurityConfigurerAdapter configuration items
- EnableWebSecurity enables automatic assembly of SpringSecurity in web scenarios
- MapperScan ({” com. Smallcannon. Spring. Security. System. Mapper “}) mybatis automatic scanning mapper package
- Define /
add
Path access requirementsadd
Permissions, /del
Need to bedel
permissions
@EnableWebSecurity
@MapperScan({"com.smallcannon.spring.security.system.mapper"})
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Resource
MyUserDetailsService myUserDetailsService;
@Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin().and().authorizeRequests().antMatchers("/add").hasAuthority("add").and().authorizeRequests().antMatchers("/del").hasAuthority("del");
}
// Set the AuthenticationProvider for the custom implementation
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(authenticationProvider());
}
// Set up the custom Provider and put the UserDetailService implementation in it
@Bean
public AuthenticationProvider authenticationProvider(a){
MyAuthenticationProvider provider = new MyAuthenticationProvider();
provider.setUserDetailsService(myUserDetailsService);
returnprovider; }}Copy the code
Start class, add two request address /add /del
@SpringBootApplication @RestController public class StudySecurityApplication { public static void main(String[] args) { SpringApplication.run(StudySecurityApplication.class, args); } @GetMapping("/add") public Object add(){ return "add"; } @GetMapping("/del") public Object del(){ return "del"; }}Copy the code
Add an administrator role to the library, associate it with the admin account, add a create permission add, and associate the administrator role with the permission add, so that when we visit our /add page, the normal page will be returned, and when we return to the del page, it will be returned with no permission
Permission to add
Administrator Role
The admin user
Associate the admin account with an administrator role
The add permission is associated with the administrator role
Start the application
After login, visit the/Add page and return add on success
Access /del page will display 403forbidden, permissions insufficient, complete!