Spring boot (2. X) + security for dynamic permission control

According to my previous blogs about learning SSM+springSecurity, I understand the basic operation of security. Next, we implement springBoot+Security to realize dynamic permission control

  • The test data in this blog database is the data from previous blogs
  • Since the operation of springBoot+security is consistent with the basic principle of SSM+springSecurity, here is a brief introduction of the project structure and some precautions

1. First, build a project of springBoot+security+mybatis

  • Add fastjson, druid and other dependencies, and the final pom file:
 <?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.2.5.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.hydata</groupId>
    <artifactId>springsecurity_springboot_01</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>springsecurity_springboot_01</name>
    <description>springSecurity01 for Spring Boot</description>

    <properties>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jdbc</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>2.1.2</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid</artifactId>
            <version>1.0.18</version>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.47</version>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
            <scope>compile</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
            <exclusions>
                <exclusion>
                    <groupId>org.junit.vintage</groupId>
                    <artifactId>junit-vintage-engine</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>
  • Write data source related information in application.yml:
spring:
  datasource:
    url: jdbc:mysql://localhost:3306/security?characterEncoding=utf-8&serverTimezone=GMT%2B8
    driver-class-name: com.mysql.cj.jdbc.Driver
    username: root
    password: 123
    type: com.alibaba.druid.pool.DruidDataSource
mybatis:
  type-aliases-package: com.hydata.springsecurity_springboot_01.entity
  mapper-locations: classpath:mybatis/mapper/*Dao.xml
  config-location: classpath:mybatis/mybatis.xml
  • Using easyCode plug-in to generate controller, service, dao layers, entity and Mapper files

    Note: here, we need to modify the SysUser generated by easyCode, implement the UserDetails interface, and implement the methods in the interface
@Data
public class SysUser implements Serializable , UserDetails {
    private static final long serialVersionUID = 573810770728994662L;
    
    private Integer id;
    
    private String username;
    
    private String realname;
    
    private String password;
    
    private Object createdate;
    
    private Object lastlogintime;
    
    private boolean enabled;
    
    private boolean accountnonexpired;
    
    private boolean accountnonlocked;
    
    private boolean credentialsnonexpired;

    private Collection<? extends GrantedAuthority> authorities;


    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return this.authorities;
    }

    @Override
    public boolean isAccountNonExpired() {
        return this.accountnonexpired;
    }

    @Override
    public boolean isAccountNonLocked() {
        return this.accountnonlocked;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return this.accountnonexpired;
    }

    @Override
    public boolean isEnabled() {
        return this.enabled;
    }
}
  • And add @ MapperScan("XXXXXXXXXX") annotation to the startup file

  • Write test cases in the test code to test whether the basic configuration is correct
@SpringBootTest
class SpringsecuritySpringboot01ApplicationTests {

    @Autowired
    private SysUserDao sysUserDao;

    @Test
    void queryById() {
        SysUser sysUser = sysUserDao.queryById(1);
        System.out.println(sysUser.toString());
    }
}
  • Test success

    2. Prepare the project for integration with security

  • Preliminary preparations (page, controller, etc.)
    Page structure:

    The code of other pages of login.html should not be pasted here:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Login page</title>
</head>
<body>
<h3>Login page</h3>
<form action="/login" method="post">
    User name:<input type="text" name="username"/><br/>
    Password:<input type="password" name="password"/><br/>
    Verification Code:<input type="text" name="imageCode"><img src="/getVerifyCode"/><br/>
    <input type="submit" value="Sign in"/>
</form>
</body>
</html>

Structure of controller layer:

MainController :

@Controller
public class MainController {



    @RequestMapping("index")
    public String index(){
        return "index";
    }


    @RequestMapping(value = "login",method = RequestMethod.GET)
    public String login(){
        return "login";
    }

    @RequestMapping("accessDeined")
    public String accessDeined(){
        return "accessDeined";
    }
}

ImageContrller :

@Controller
@Slf4j
public class ImageContrller {

    /* Get captcha picture*/

    @RequestMapping("/getVerifyCode")
    public void getVerificationCode(HttpServletResponse response, HttpServletRequest request) {
        OutputStream os = null;
        try {

            int width = 200;

            int height = 69;

            BufferedImage verifyImg = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);

            //Generate initial image corresponding to width and height

            String randomText = VerifyCode.drawRandomText(width, height, verifyImg);

            //A separate class method is encapsulated for code reuse.

            //The function is to generate verification code characters and add noise and interference lines. The return value is verification code characters

            request.getSession().setAttribute("verifyCode", randomText);

            response.setContentType("image/png");//The response content type must be set to picture, otherwise the foreground will not recognize it

            os = response.getOutputStream(); //Get file output stream

            ImageIO.write(verifyImg, "png", os);//Output picture stream

            os.flush();


        } catch (IOException e) {

            log.error(e.getMessage());

            e.printStackTrace();

        } finally {
            try {
                if (os != null) {
                    os.close();//Closed flow
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

    }
}

VerifyCode :

public class VerifyCode {

    public static  String drawRandomText(int width, int height, BufferedImage verifyImg) {

        Graphics2D graphics = (Graphics2D)verifyImg.getGraphics();

        graphics.setColor(Color.WHITE);//Set brush color - verification code background color

        graphics.fillRect(0, 0, width, height);//Fill background

        graphics.setFont(new Font("Microsoft YaHei", Font.BOLD, 40));

        //Combination of numbers and letters

        String baseNumLetter= "123456789abcdefghijklmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ";

        StringBuffer sBuffer = new StringBuffer();

        int x = 10;  //x coordinate of the origin of rotation

        String ch = "";

        Random random = new Random();

        for(int i = 0;i < 4;i++){

            graphics.setColor(getRandomColor());

            //Set font rotation angle

            int degree = random.nextInt() % 30;  //Angle less than 30 degrees

            int dot = random.nextInt(baseNumLetter.length());

            ch = baseNumLetter.charAt(dot) + "";

            sBuffer.append(ch);

            //Forward rotation

            graphics.rotate(degree * Math.PI / 180, x, 45);

            graphics.drawString(ch, x, 45);

            //Reverse rotation

            graphics.rotate(-degree * Math.PI / 180, x, 45);

            x += 48;

        }

        //Painting interference line

        for (int i = 0; i <6; i++) {

            // Set random color

            graphics.setColor(getRandomColor());

            // Random drawing

            graphics.drawLine(random.nextInt(width), random.nextInt(height),

                    random.nextInt(width), random.nextInt(height));

        }

        //add noise

        for(int i=0;i<30;i++){

            int x1 = random.nextInt(width);

            int y1 = random.nextInt(height);

            graphics.setColor(getRandomColor());

            graphics.fillRect(x1, y1, 2,2);

        }

        return sBuffer.toString();

    }

    /**
     * Random color selection
     */

    private static Color getRandomColor() {

        Random ran = new Random();

        Color color = new Color(ran.nextInt(256),

                ran.nextInt(256), ran.nextInt(256));

        return color;

    }

}

3. Then formally integrate

  • Create a new class to inherit WebSecurityConfigurerAdapter (abstract class). The purpose of this class is to realize the function of springSecurity.xml in SSM+SpringSecurity. Then rewrite the methods. There are two common methods:
  • protected void configure(AuthenticationManagerBuilder auth): you can guess from the name that this method is for < security: authentication manager > in the configuration file, which is used to specify the authentication class, encryption method, etc
  • Protected void configure (HTTP security HTTP): by looking at the name, we can guess that this method is for < security: http > in the configuration file, which is used to specify the login mode (FormLogin, HttpBasic), login page, resource access permission, release resources, specify the execution order for the custom filter, exit, remember me, etc
@Configuration
@EnableWebSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {

    @Autowired
    private SysUserServiceImpl sysUserService;

    @Autowired
    private PasswordEncoder passwordEncoder;

    @Autowired
    private MyAuthenticationSuccessHandler myAuthenticationSuccessHandler;
    @Autowired
    private MyAuthenticationFailureHandler myAuthenticationFailureHandler;
    @Autowired
    private MyOnecPerFilter myOnecPerFilter;

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(sysUserService).passwordEncoder(passwordEncoder);

    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {

       http.authorizeRequests()
               .antMatchers("/login","/index","/getVerifyCode").permitAll()
//               Note: the hasRole, hasAnyRole method will be automatically prefixed with "ROLE" and hasAnyAuthority will not
               .antMatchers("/sysUser/add").hasRole("ADD")
               .antMatchers("/sysUser/list").hasAnyAuthority("ROLE_LIST")
               .antMatchers("/sysUser/update").hasAnyAuthority("ROLE_UPDATE")
               .antMatchers("/sysUser/delete").hasAnyAuthority("ROLE_DELETE")
               .antMatchers("/**").authenticated()
               .and()
               .formLogin().loginPage("/login")
//               The page to jump after successful authentication does not coexist with the successHandler
//               .defaultSuccessUrl("/index")
               .successHandler(myAuthenticationSuccessHandler).failureHandler(myAuthenticationFailureHandler)
               .and()
               .csrf().disable();
        http.addFilterBefore(myOnecPerFilter,UsernamePasswordAuthenticationFilter.class);

    }


    @Bean
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();

    }


}

From the code, we can see that we have specified the encryption method BCryptPasswordEncoder, the user-defined authentication logic SysUserService (this class implements the UserDetailsService interface, rewrites the loadUserByUsername method, and defines the authentication logic in the method), the processor myAuthenticationSuccessHandler after the authentication succeeds, and the processor myAuthenticationFailureHandler after the authentication fails, so as to And we put the custom filter myOnecPerFilter in front of UsernamePasswordAuthenticationFilter.

  • Next, we will show the above customized logic

Processor myAuthenticationFailureHandler after authentication failure:

@Component
public class MyAuthenticationFailureHandler implements AuthenticationFailureHandler {
    @Override
    public void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
        Map<String,Object> map=new HashMap<>(2);
        map.put("success",false);
        if(e.getMessage()!=null){
            if(e.getMessage().contains("UserDetailsService returned null")){
                map.put("mes","user name does not exist");
            }else if(e.getMessage().contains("Bad credentials")){
                map.put("mes","Wrong account or password");
            }else{
                map.put("mes",e.getMessage());
            }
        }
        String result = JSONObject.toJSONString(map);
        httpServletResponse.setContentType("text/json;charset=UTF-8");
        httpServletResponse.getWriter().write(result);
    }
}

myAuthenticationSuccessHandler after authentication:

@Component
@Slf4j
public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authentication) throws IOException, ServletException {
        chain.doFilter(request,response);
    }

    @Override
    public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
        Map<String,Object> map=new HashMap<>(2);
        map.put("success",true);
        map.put("mes","Landing successfully");
        String result = JSONObject.toJSONString(map);
        httpServletResponse.setContentType("text/json;charset=UTF-8");
        httpServletResponse.getWriter().write(result);
    }


}

Custom filter myOnecPerFilter:

@Component
@Slf4j
public class MyOnecPerFilter extends OncePerRequestFilter {

    @Autowired
    private MyAuthenticationFailureHandler authenticationFailureHandler;


    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException {
        String requestURI = request.getRequestURI();
        String method = request.getMethod();
        log.debug("URL:" + requestURI + "----------METHOD:" + method);
        if (requestURI.contains("login") && method.equalsIgnoreCase("POST")) {
            try {
                String imageCode = request.getParameter("imageCode");
                if (StringUtils.isEmpty(imageCode)) {
                    throw new MyAuthenticationException("Please enter the verification code");
                }
                String verifyCode = (String) request.getSession().getAttribute("verifyCode");
                if (!imageCode.equalsIgnoreCase(verifyCode)) {
                    throw new MyAuthenticationException("Please enter the correct verification code");
                }
            } catch (AuthenticationException e) {
                authenticationFailureHandler.onAuthenticationFailure(request, response, e);
                return;
            }

        }
        chain.doFilter(request, response);
    }
}

Exception used in custom filter:

public class MyAuthenticationException extends AuthenticationException {
    public MyAuthenticationException(String msg, Throwable t) {
        super(msg, t);
    }

    public MyAuthenticationException(String msg) {
        super(msg);
    }
}

User defined authentication logic in SysUserServiceImpl (this class implements the UserDetailsService interface, rewrites the loadUserByUsername method, and defines the authentication logic in the method):

@Service("sysUserService")
public class SysUserServiceImpl implements SysUserService, UserDetailsService {
    @Resource
    private SysUserDao sysUserDao;

    @Override
    public UserDetails loadUserByUsername(String userName) throws UsernameNotFoundException {
        SysUser sysUser = sysUserDao.queryByUserName(userName);
        List<GrantedAuthority> list=new ArrayList();
        if(sysUser!=null){
            List<SysPermission> permTags = sysUserDao.queryPermTagsByUserName(userName);
            for (SysPermission permTag : permTags) {
                list.add(new SimpleGrantedAuthority(permTag.getPermtag()));
            }
            sysUser.setAuthorities(list);
        }

        return sysUser;
    }
}

Next, sysUserDao defines two query methods and writes sql
sysUserDao.queryByUserName(userName);
sysUserDao.queryPermTagsByUserName(userName);

<!--Query single-->
    <select id="queryByUserName" resultMap="SysUserMap" parameterType="string">
        select
          id, username, realname, password, createDate, lastLoginTime, enabled, accountNonExpired, accountNonLocked, credentialsNonExpired
        from security.sys_user
        where username = #{username}
    </select>
    <!--Query all permissions based on user name-->
    <select id="queryPermTagsByUserName" resultType="sysPermission" parameterType="string">
              SELECT sys_permission.id,sys_permission.permName,sys_permission.permTag
	      from (select id ,username from sys_user where username=#{userName}) user
		  join sys_user_role user_role on user.id=user_role.user_id
		  join sys_role_permission role_permission on user_role.role_id=role_permission.role_id
		  join sys_permission on sys_permission.id=role_permission.perm_id

    </select>

Finally, visit localhost:8080/login for login test

4. Method and attention

  • Permission method description

Method implication

access(String) parameter is spiel, if it returns true, access is allowed.)

anonymous() allows anonymous access

authorizerequests() restricts requests to be signed

anyRequest() restricts any request

hasAnyrole(String… )Give access to multiple roles (roles are automatically prefixed with "ROLE")

hasRole(String) gives access to a ROLE (the ROLE will automatically be prefixed with "ROLE")

permitAll() allows access unconditionally

and() conjunctions and remove the pre qualification

httpbasico() enables HTTP basic authentication of browser

formLogin() enables Spring Security's default login page

not() negates access to other methods

fullyAuthenticated() allows access if it is full authentication (not Remember--me)

dentAll() unconditionally does not allow any access

haslpAddress(String) allows access if it is the given IP address

rememberme() users are allowed to access through the remember me function authentication

hasAuthority(String) if it is a given ROLE, access is allowed (the prefix "ROLE" will not be added automatically)

hasAnyAuthority(String) if any one of the given roles is allowed to access (the prefix "ROLE" will not be added automatically)
  • The spring EL expression can be used in access(String). Return true to allow access:
    For example:
http.authorizeRequests()
				//Allow all people (whether authenticated or not) to access login, index, getVerifyCode
                .antMatchers("/login", "/index", "/getVerifyCode").permitAll()
                //Allow role to be role add or role admin and full login (not remember me login) to access / sysUser/add
                .antMatchers("/sysUser/add").access("hasAuthority('ROLE_ADD') || hasRole('ADMIN') && isFullyAuthenticated()")

Method implication

authentication() user authentication object

Deny all() denies any access

hasanyrole((String… )Whether the object property listed in the parameter exists for the current user

hasRole(String) whether the current user has a role

haslpAddress(String) whether to request from the specified IP

Is anonymous() accessed anonymously

isAuthenticated() whether the user has passed the authentication signature isFullyAuthenticatedO whether the user is a complete authentication, that is, the authentication that the non Remember me authentication function has passed

Whether isRememberMe() is verified by the "remember me" function

permitAll() unconditionally allows any access

principal() user's principal object
  • Encryption method of Pbkdf2passwordEncoder
    This encryption method is to store the key in the server and encrypt and decrypt the password entered by the user through the key
    In application.yml, add:
 system: 
 	user: 
 		password: 
 			secret: xxxx(Key)

In the instantiation encryption mode, the

  @Value("${system.user.password.secret}")
    private String secret;

    @Bean
    public PasswordEncoder passwordEncoder(){
        return new Pbkdf2PasswordEncoder(this.secret);

    }
Published 12 original articles, won praise 3, visited 1697
Private letter follow

Tags: Spring Mybatis Maven MySQL

Posted on Sat, 14 Mar 2020 03:03:51 -0700 by jasper