SpringSecurity + custom login interface + JwtToken + interceptor + front and back end separation

1: Project background: after fishing, I'm going to write a project to consolidate myself. There are some cd problems in the process of code project. You may have encountered them. Share my summary again. I want to use spring cloud + vue to write a front and back-end separated project. Because I haven't learned vue yet, hahaha, hahaha, so I make a simple login page with html page to imitate secure login. Ha ha ha ha ha ha ha.          

2: Problems encountered: the login page is customized, and the submission path of the login form is customized (request to the login interface of the controller layer). Click Submit. The request does not enter the customized login interface, but directly enters the method of implementing UserDetailService.

(later, I rewritten the http configuration in the configure method in MySercurityConfig,

In addition, the following method was used before. When logging in, it was called, and then it was found that it could not go to the userdetailserviceimpl method that implements UserDetailsService defined by itself. Later, this method was deleted and useless. This is actually the method of obtaining user information according to user name)

@Overrite
@Bean
public UserDeailService userDeailService() {
    return username -> {
        Admin admin = adminService.getAdminService(username);
        return admin;
    }
    return null;
}

Another is to rewrite the interceptor and generate a token to verify the login.

Let's give you a detailed analysis of my login module

3: Import dependency: select the appropriate version. I use 2.3.7 RELEASE

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.9.0</version>
</dependency>

configuration file

jwt:
  #JWT stored request header
  tokenHeader: Authorization #key
  #Signature used for JWT encryption and decryption
  secret: handsome
  #JWT failure time
  expiration: 3600
  #Get the start in JWT load
  tokenHead: Bearer #Start with value

4: initial experience of spring security: after introducing dependency, start the project directly, visit your project path, and you will enter the security login page of spring security. User name: user, password: it will be printed in the console after the project is started, and you can log in directly. But people usually don't use the login page that comes with spring security.

5: Customize the login page: next, we create a class that inherits WebSecurityConfigurerAdapter to implement the login page. The custom code is as follows.

You need to write a login interface first: you can write a login in html first html, put it under static under resource, write a login interface of controller layer, and then write the configuration class to realize the user-defined login page.

package com.power.auth.securityconfig;

import com.power.auth.filter.LoginFilter;
import com.power.auth.filter.RestAuthorizationEntryPoint;
import com.power.auth.filter.RestfulAccessDenieHandler;
import com.power.auth.utils.RealPasswordEncoding;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

/**
 * @author HandsomeZhang
 * @date 2021/12/13 15:51
 * Description:
 */
@Configuration//Configuration class annotation. The configuration class will be loaded when the project is started. You can make a breakpoint to start it
public class MySercurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .authorizeRequests()//Request authorization
                .antMatchers("/loginController/getToken").permitAll()//This is the submission path of the custom form, that is, the login interface of our controller layer, which allows access
                .antMatchers("/login.html").permitAll()//Customize the login interface to allow access
                .anyRequest().authenticated()//Other requests are blocked
                .and()
                //Custom login form
                .formLogin()
                .loginPage("/login.html")//Login form, but you can't access it without authorization
                .loginProcessingUrl("/loginController/getToken/")//Form submission path
                .and()
                .httpBasic()
                .and().csrf().disable();//Turn off firewall
    }
}

At this time, when you start the project and access the project address, you will come to the customized login page. Moreover, no matter which request path you access under the project, you will jump to the login page, but it will not be after adding an interceptor. You need to access the login interface, which can be solved, as described later. If you want to release the interface in the non login state, you can set the white list (this is not used yet).

6: Define a JwtToken tool class: it can generate verification tokens

package com.power.auth.utils;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;

import java.util.Date;
import java.util.HashMap;
import java.util.Map;

/**
 * JWT Tool class
 */
@Component
public class JwtTokenUtil {
    private static final String CLAIM_KEY_USERNAME="sub";//claims will get the user name when parsing the token according to this sub, and can only fill in the sub
    private static final String CLAIM_KEY_CREATED="created";//Creation time
    @Value("${jwt.secret}")
    private String secret;//Key used for encryption and decryption
    @Value("${jwt.expiration}")
    private Integer expiration;//Effective time

    /**
     * Generate a token based on user information
     * @param userDetails
     * @return
     */
    public String generateToken(UserDetails userDetails) {//How did you get the user information?
        Map<String, Object> claims = new HashMap<>();
        claims.put(CLAIM_KEY_USERNAME, userDetails.getUsername());//Get the user name
        claims.put(CLAIM_KEY_CREATED, new Date());//Landing time
        return generateToken(claims);
    }

    /**
     * Obtain the login user name according to the token
     * @param token
     * @return
     */
    public String getUserNameFromToken(String token) {
        String userName;

        try {
            Claims claims = getClaimsFromToken(token);//Get the load
            userName = claims.getSubject();//Get the user name
        } catch (Exception e) {
            userName = null;
        }
        return userName;
    }

    /**
     * Judge whether the token is valid
     * @param token
     * @return
     *  If the user name is incorrect or the token expires, it will be regarded as invalid
     *  Return true to be valid
     */
    public boolean tokenExpired(String token, UserDetails userDetails) {
        String userName = getUserNameFromToken(token);
        return userName.equals(userDetails.getUsername()) && isTokenExpired(token);
    }

    /**
     * Judge whether the token can be refreshed
     * If the token expires, it can be refreshed
     * @param token
     * @return
     *         false Is expired
     */
    public boolean isRefresh(String token) {
        return !isTokenExpired(token);
    }

    /**
     * Refresh token expiration time
     * @param token
     * @return
     */
    public String refreshTokenExpiredTime(String token) {
        Claims claims = getClaimsFromToken(token);
        claims.put(CLAIM_KEY_CREATED, new Date());//New login time
        return generateToken(claims);
    }

    /**
     * Get token expiration time
     * @param token
     * @return
     */
    private boolean isTokenExpired(String token) {
        Claims claims = getClaimsFromToken(token);
        Date expiration = claims.getExpiration();//Get the expiration date put in when generating the token
        return new Date().before(expiration);
    }

    /**
     * Get the load according to the token
     * @param token
     * @return
     */
    private Claims getClaimsFromToken(String token) {
        Claims claims = null;
        try {
            claims = Jwts.parser()
                    .setSigningKey(secret)//secret key
                    .parseClaimsJws(token)
                    .getBody();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return claims;
    }

    /**
     * Generate JWT token according to load
     * @param claims
     * @return
     */
    private String generateToken(Map<String, Object> claims) {
        byte[] secretKey = secret.getBytes();
        //Generate token
        return Jwts.builder()
                .setClaims(claims)//load
                .setExpiration(generateExpirationDate())//Failure time
                .signWith(SignatureAlgorithm.HS512, secretKey)
                .compact();
    }

    /**
     * Generation token expiration time
     * @return
     */
    private Date generateExpirationDate() {
        return new Date(120000);//The expiration time is two hours
    }
}

7: I have rewritten the password encryption rules of sercurity. I use the md5 tool class, which can be found on the Internet

package com.power.auth.utils;

import com.power.common.utils.MD5Utils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.crypto.password.PasswordEncoder;

/**
 * @author HandsomeZhang
 * @date 2021/12/12 13:08
 * Description:
 */
public class RealPasswordEncoding implements PasswordEncoder {
    public static Logger logger = LoggerFactory.getLogger(RealPasswordEncoding.class);

    /**
     * Password encryption
     * @param charSequence
     * @return
     */
    @Override
    public String encode(CharSequence charSequence) {
        logger.info("Custom password encryption>>>>>>>>");

        String passwordSafe = null;
        try {
            String password = (String) charSequence;
            passwordSafe = MD5Utils.md5Encode(password);
        } catch (Exception e) {
            logger.info("Encryption failed");
            e.printStackTrace();
        }
        return passwordSafe;
    }

    /**
     * password verifiers 
     * @param charSequence Unencrypted password
     * @param s Encrypted password
     * @return
     */
    @Override
    public boolean matches(CharSequence charSequence, String s) {
        logger.info("Custom password verification>>>>>>>>");

        String passwordSafe = null;
        try {
            String password = (String) charSequence;
            passwordSafe = MD5Utils.md5Encode(password);
            String s1 = MD5Utils.md5Encode("Zjg123...");
            logger.info(s1);
        } catch (Exception e) {
            logger.info("Encryption failed");
            e.printStackTrace();
        }
        logger.info(passwordSafe);

        String password = (String) charSequence;
        boolean b = MD5Utils.matches(password, s);
        if (b == true) {
            logger.info("Password comparison succeeded");
        } else {
            logger.info("Password comparison failed");
        }
        return b;
    }
}

8: Define an implementation class of UserDetails to replace the User provided by springsecurity to receive the parameters you want

package com.power.auth.entity;

import com.power.basic.entity.User;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.security.core.CredentialsContainer;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import java.io.Serializable;
import java.util.Collection;

/**
 * Custom LoginUserDetials can replace User to put more information
 */

@Data
@NoArgsConstructor
public class LoginUserDetials implements UserDetails, Serializable {
    private Long id;
    private String username;
    private String password;

    private Collection<? extends GrantedAuthority> authorities;
    private boolean isAccountNonExpired;
    private boolean isAccountNonLocked;
    private boolean isCredentialsNonExpired;
    private boolean isEnabled;

    public LoginUserDetials(User user, Collection<? extends GrantedAuthority> authorities) {
        this.setUsername(user.getUserName());
        this.setId(user.getUserId());
        this.setPassword(user.getUserPassword());
        this.setAuthorities(authorities);
        this.setAccountNonExpired(true);
        this.setAccountNonLocked(true);
        this.setCredentialsNonExpired(true);
        this.setEnabled(true);
    }

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

    @Override
    public String getPassword() {
        return this.password;
    }

    @Override
    public String getUsername() {
        return this.username;
    }

    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }


    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return true;
    }

}

9: Verify the login user information and implement the UserDetailsService interface provided by security

package com.power.auth.serviceimpl;

import com.alibaba.fastjson.JSONObject;
import com.power.auth.entity.LoginUserDetials;
import com.power.auth.utils.JwtTokenUtil;
import com.power.basic.api.UserServiceFeign;
import com.power.basic.entity.User;
import com.power.common.pojo.ResultPublic;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.authentication.DisabledException;
import org.springframework.security.authentication.LockedException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;

import java.util.HashMap;
import java.util.Map;

/**
 * @author HandsomeZhang
 * @date 2021/12/13 16:57
 * Description:
 */
@Service
public class UserDetialServiceImpl implements UserDetailsService {
    //From database
    @Autowired(required = false)
    private UserServiceFeign userServiceFeign;
    @Autowired
    private PasswordEncoder passwordEncoder;
    @Autowired
    private JwtTokenUtil jwtTokenUtil;
    @Value("${jwt.tokenHead}")
    private String tokenHead;

    //Verify login user information
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        JSONObject jsonObject = new JSONObject();
        jsonObject.put("userName", username);
        //Query the data to determine whether the user name exists
        ResultPublic<User> getUser = userServiceFeign.getByUserName(jsonObject);
        User user = getUser.getData();
        if (user == null ) {
            throw new UsernameNotFoundException("user name does not exist");
        }
        //Parse the queried password and encrypt it when registering
        LoginUserDetials loginUserDetials = new LoginUserDetials(user,
                AuthorityUtils.commaSeparatedStringToAuthorityList("admin"));

        //Judge whether the user status is normal
        if (!loginUserDetials.isEnabled()) {
            throw new DisabledException("The account has been disabled!");
        } else if (!loginUserDetials.isAccountNonLocked()) {
            throw new LockedException("The account has been locked!");
        }
        return loginUserDetials;
    }

    //Create a token when logging in
    public ResultPublic createToken (UserDetails userDetails, String password) {
        if (null == userDetails || !passwordEncoder.matches(password, userDetails.getPassword())) {
            return ResultPublic.fail("Incorrect user name or password");
        }
        //If the login object is placed in the full text of spring security, some problems may occur if it is not placed
        //Update security login user object
        UsernamePasswordAuthenticationToken authenticationToken = new
                UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());//Parameters: user information, password, permission list,
        SecurityContextHolder.getContext().setAuthentication(authenticationToken);//Put the token into the Security global
        //Generate token
        Map<String, Object> tokenMap = new HashMap<>();
        String token = jwtTokenUtil.generateToken(userDetails);
        tokenMap.put("token", token);//Put in the token
        tokenMap.put("tokenHead", tokenHead);//Put in Bearer
        //After successful login, the front end will splice the two values in the map, and the subsequent requests will be carried in the request header
        return ResultPublic.success("Login succeeded", tokenMap);
    }
}

10: Define a login interface: each login will generate a token

package com.power.auth.controller;

import com.power.auth.serviceimpl.UserDetialServiceImpl;
import com.power.common.pojo.ResultPublic;
import io.swagger.annotations.Api;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.web.bind.annotation.*;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;

@Api(tags = "LoginController")
@RestController
@RequestMapping(path = "/loginController")
public class LoginController {
    @Resource
    private UserDetailsService userDetailsService;
    @Autowired
    private UserDetialServiceImpl userDetialServiceImpl;

    public static Logger logger = LoggerFactory.getLogger(LoginController.class);

    @PostMapping(path = "/getToken")
    public ResultPublic getToken(@RequestParam String username,
                           @RequestParam String password,
                            HttpServletRequest request
    ) {
        //Database query and comparison of login user status information
        UserDetails userDetails = userDetailsService.loadUserByUsername(username);
        //Generate token
        ResultPublic token = userDetialServiceImpl.createToken(userDetails, password);
        return token;
    }

    @GetMapping(path = "/loginOut")
    public ResultPublic loginOut() {
        return ResultPublic.success("Exit successfully");
    }

}

11: Define an interceptor. Each time you access the interface, you will pass through the interceptor to verify whether the token is invalid

package com.power.auth.filter;

import com.power.auth.utils.JwtTokenUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.web.filter.OncePerRequestFilter;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * @author HandsomeZhang
 * @date 2021/12/13 20:39
 * Description:
 */

public class LoginFilter extends OncePerRequestFilter {
    @Value("${jwt.tokenHeader}")
    private String tokenHeader;//Request header brought by the front end
    @Value("${jwt.tokenHead}")
    private String tokenHead;
    @Autowired
    private JwtTokenUtil jwtTokenUtil;
    @Autowired
    private UserDetailsService userDetailsService;

    public static Logger logger = LoggerFactory.getLogger(LoginFilter.class);

    //Each visit comes here to intercept and verify the token
    //If there is a token and the user status is normal, but the token expires, reset the user object
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        logger.info("Entry interceptor JwtAuthencationTokenFilter!!!");

        //Example of token from the front end: authorization = token generated by bearer + JWT tool class
        String fakeToken = request.getHeader(tokenHeader);
        //If the token is not empty and starts with Bearer
        if (null != fakeToken && fakeToken.startsWith(tokenHead)) {
            String authToken = fakeToken.substring(tokenHeader.length());//Intercept Bearer and get the following token
            String userName = jwtTokenUtil.getUserNameFromToken(authToken);//Get user name from token
            //The token has a user name but is not logged in
            if (null != userName && null == SecurityContextHolder.getContext().getAuthentication()) {
                UserDetails userDetails = userDetailsService.loadUserByUsername(userName);//Query user information according to user name
                //Verify whether the token expires and reset the user object
                if (jwtTokenUtil.tokenExpired(authToken, userDetails)) {
                    UsernamePasswordAuthenticationToken authenticationToken = new
                            UsernamePasswordAuthenticationToken(userDetails, null,userDetails.getAuthorities());
                    authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
                    SecurityContextHolder.getContext().setAuthentication(authenticationToken);
                }
            }
        }
        logger.info("JwtAuthencationTokenFilter Interceptor release!!!");
        filterChain.doFilter(request, response);
    }
}

12: You can also define two exception handling schemes: I have defined RestAuthorizationEntryPoint and RestfulAccessDenieHandler here, which are used in MySercurityConfig

Keywords: Java Spring Spring Security IDEA

Added by bashaash on Mon, 24 Jan 2022 22:30:42 +0200