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.ymlconfiguration

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-generatorDependency 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 aboveUsernamePasswordAuthenticationFiltertheauthenticationThe implementation of theUsernamePasswordAuthenticationTokenRight? This is going to match by default toDaoAuthenticationProvider.DaoAuthenticationProviderIt’s not implemented by itselfsupportsMethod, the real implementation isAbstractUserDetailsAuthenticationProviderAnd theAbstractUserDetailsAuthenticationProviderThe 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 /addPath access requirementsaddPermissions, /delNeed to bedelpermissions
@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!