Share Spring Security source level login process based on company project

Keywords: Spring Mobile JSON Java

In the front

First of all, I contacted this framework and soon remembered that when I first came in, my tutor also told me the process, because I didn't use this thing in the previous project, so I was confused. Recently, I just met some login requirements, and I took advantage of my interest to learn for a while. Today, I have the courage to share a wave. Then the goal of this sharing is not the source code of the whole framework, but more about the login process. This sharing should be more practical, after all, everyone can touch it.

Expected target

  • Basic level: familiar with the application of Spring Security in the project
  • Intermediate level: familiar with source level login process
  • Advanced: solve some problems by looking at the source code
  • Additional: authorization control

What is Spring Security?

Official introduction

  • Spring Security is a powerful and highly customizable authentication and access-control framework. It is the de-facto standard for securing Spring-based applications.

Spring Security is a powerful and highly customizable authentication and access control framework, which is the de facto standard to protect spring based applications.

  • Spring Security is a framework that focuses on providing both authentication and authorization to Java applications. Like all Spring projects, the real power of Spring Security is found in how easily it can be extended to meet custom requirements.

Spring Security is a framework that focuses on providing authentication and authorization for Java applications. Like all spring projects, the real strength of Spring Security is that it can be easily extended to meet custom requirements.

Generally speaking

  • First of all, our front-end applications need to bring access credentials (tokens) to access the back-end resources, such as the back-end interface. We can't directly expose the interface resources to the front-end applications, which has great security risks.
  • This framework is to facilitate the process of obtaining credentials, with the emphasis on authentication and authorization. Authentication is to authenticate your identity, such as verifying whether the user name and password are correct, whether the mobile phone number is correct, and whether the mobile phone number user exists in our library. The purpose of authentication is to authorize, and the result of authorization is to give an access credential to the front end, such as to the front end JWT tokens.
  • The front-end has the token, and each request can access the background resource with the token. Of course, before each access to the resource, the background will check the token to determine whether the token is valid or not has expired and so on.

The application of Spring Security in the project

  • Based on the company project, take SMS verification code login as an example

Classes to create

  • Create a filter to filter and intercept login requests. In general, you can inherit AbstractAuthenticationProcessingFilter, such as:
public class VipMemberAuthenticationFilter extends AbstractAuthenticationProcessingFilter {

	public VipMemberAuthenticationFilter() {
		super(new AntPathRequestMatcher("/vip/members:login", "POST"));
	}
	private final String sessionCachePrefix = "vip:s";

	@Override
	public void afterPropertiesSet() {
		Assert.notNull(getAuthenticationManager(), "AuthenticationManager must be specified");
		Assert.notNull(getSuccessHandler(), "AuthenticationSuccessHandler must be specified");
		Assert.notNull(getFailureHandler(), "AuthenticationFailureHandler must be specified");
	}

	@Override
	public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
			throws org.springframework.security.core.AuthenticationException, IOException, ServletException {

		String body = StreamUtils.copyToString(request.getInputStream(), Charset.forName("UTF-8"));
		String mobile = null, smsCode = null;
		if(StringUtils.hasText(body)) {
		    JSONObject jsonObj = JSON.parseObject(body);
		    mobile = jsonObj.getString("mobile");
		    smsCode = jsonObj.getString("smsCode");
		}

		// Verify that the SMS verification code is correct
		if (StringUtils.isEmpty(mobile)) {
			throw AuthenticationException.USERNAME_IS_NULL;
		}
		String verifyCodeInCache = JedisUtil.getJedisInstance().execGetFromCache(sessionCachePrefix + ":" + mobile + ":svc");
		if (StringUtils.isEmpty(smsCode) || !smsCode.equalsIgnoreCase(verifyCodeInCache)) {
			throw AuthenticationException.VERIFY_CODE_INVALID;
		}

		VipMemberUsernamePasswordAuthenToken authRequest = new VipMemberUsernamePasswordAuthenToken(
				mobile.trim(), smsCode);
		
		return this.getAuthenticationManager().authenticate(authRequest);
	}
}
  • Create a Provider to authenticate login requests and implement AuthenticationProvider. All providers are managed by AuthenticationManager, such as:
public class VipMemberJsonAuthenticationProvider implements AuthenticationProvider{

	private VipMemberFacade vipMemberFacade;
	public VipMemberJsonAuthenticationProvider(VipMemberFacade vipMemberFacade) {
		this.vipMemberFacade = vipMemberFacade;
	}

	@Override
	public Authentication authenticate(Authentication authentication) throws AuthenticationException {

		String mobile = authentication.getPrincipal().toString();
		VipMemberQueryRequest vipMemberQueryRequest = new VipMemberQueryRequest();
		vipMemberQueryRequest.setMobile(mobile);
		vipMemberQueryRequest.setDeleted(false);
		vipMemberQueryRequest.setStatusList(Arrays.asList(MemberStatus.ENABLE.getCode(), MemberStatus.MANUAL_BLOCK.getCode(), MemberStatus.SYSTEM_BLOCK.getCode()));
		PageCommonResponse<VipMemberDTO> pageResp = vipMemberFacade.queryMember(vipMemberQueryRequest);
		//Remove the error code that the user does not exist
		if (!Constants.OPERATE_SUCCESS.equals(pageResp.getCode())) {
			throw new AuthenticationException(pageResp.getCode(), pageResp.getEngDesc(), pageResp.getChnDesc());
		}
		
		// balabala... A bunch of user information codes

		//Remove cached user information when logging in again
		JedisUtil.getJedisInstance().execDelToCache(com.meicloud.mcu.sria.wxclient.constant.Constants.SYS_USER_CACHE_PREFIX + vipMemberDTO.getId() + "_BA");

		LoginDTO loginDTO = new LoginDTO();
		loginDTO.setId(vipMemberDTO.getId());

		loginDTO.setBelongToId(vipMemberDTO.getVipMemberBelongToDTO().getBelongToId());
		JwtAuthenticationToken token = new JwtAuthenticationToken(loginDTO, null, null);
		return token;
	}

	@Override
	public boolean supports(Class<?> authentication) {
		return authentication.isAssignableFrom(VipMemberUsernamePasswordAuthenToken.class);
	}
}
  • Create a login success processor and a login failure processor, and implement the AuthenticationSuccessHandler and AuthenticationFailureHandler interfaces respectively, such as:
/**
 * The successful processor mainly generates a JWT and returns it to the front end
 */
public class VipMemberJsonLoginSuccessHandler implements AuthenticationSuccessHandler{

	private SecurityConfig securityConfig;
	public VipMemberJsonLoginSuccessHandler(SecurityConfig securityConfig) {
		this.securityConfig = securityConfig;
	}

	@Override
	public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
			Authentication authentication) throws IOException, ServletException {

		LoginDTO user = (LoginDTO)authentication.getPrincipal();
		user.setUserType("BA");
		Date date = new Date(System.currentTimeMillis() + securityConfig.getTokenExpireTimeInSecond() * 1000);

		Algorithm algorithm = Algorithm.HMAC256(securityConfig.getTokenEncryptSalt());

		String token = JWT.create()
				.withSubject(user.getId()+"_"+"BA")
				.withClaim(LoginDTO.BELONG_TO_ID, user.getBelongToId())
				.withExpiresAt(date)
				.withIssuedAt(new Date())
				.sign(algorithm);

		BaseResponse<LoginDTO> baseResponse = new BaseResponse<>();
		baseResponse.setCode(Constants.OPERATE_SUCCESS);
		baseResponse.setEngDesc("success");
		baseResponse.setChnDesc("Login successful");
		baseResponse.setContent(user);
		response.setHeader(securityConfig.getTokenName(), token);
		response.setCharacterEncoding("UTF-8");
		response.getWriter().print(JSON.toJSONString(baseResponse));
	}
}

/**
 * Failure processor, return some error codes and error prompts
 */
public class HttpStatusLoginFailureHandler implements AuthenticationFailureHandler{

	@Override
	public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
										org.springframework.security.core.AuthenticationException exception) throws IOException, ServletException {

		BaseResponse baseResponse = new BaseResponse();

		if (exception instanceof AuthenticationException) {
			AuthenticationException e = (AuthenticationException)exception;
			baseResponse.setCode(e.getCode());
			baseResponse.setChnDesc(e.getChnDesc());
			baseResponse.setEngDesc(e.getEngDesc());
		} else {
			baseResponse.setEngDesc(exception.getMessage());
		}

		if (AuthenticationException.JWT_EXPIRED.getCode().equals(baseResponse.getCode())
				|| AuthenticationException.JWT_IS_EMPTY.getCode().equals(baseResponse.getCode())
				|| AuthenticationException.USER_NOT_EXISTS.getCode().equals(baseResponse.getCode())
				|| AuthenticationException.USER_DISABLED.getCode().equals(baseResponse.getCode())
				|| AuthenticationException.JWT_FORMAT_ERROR.getCode().equals(baseResponse.getCode())) {
			response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
		}

		response.setContentType("application/json");
		response.setCharacterEncoding(Charset.defaultCharset().displayName());
		String responseText = JSON.toJSONString(baseResponse);
		response.getWriter().print(responseText);
	}
}
  • To create an Authentication Token, you need to implement the Authentication interface. Generally, we inherit its implementation class, which is used to encapsulate the Authentication Token, such as:
/**
 * We can inherit some classes for our convenience. Of course, we must implement the Authentication interface in the end
 */
public class VipMemberUsernamePasswordAuthenToken extends UsernamePasswordAuthenticationToken {

    public VipMemberUsernamePasswordAuthenToken(Object principal, Object credentials) {
        super(principal, credentials);
    }

    @Override
    public boolean implies(Subject subject) {
        return super.implies(subject);
    }
}

Configuration to add

  • Configure the filter, that is, add the filter to the filter chain, configure the login success processor and failure processor at the same time, create a configuration class, and implement AbstractHttpConfigurer, such as:
public class JsonLoginConfigurer<T extends JsonLoginConfigurer<T, B>, B extends HttpSecurityBuilder<B>> extends AbstractHttpConfigurer<T, B>  {

	private SysUserFacade sysUserFacade;
	private SecurityConfig securityConfig;

	public JsonLoginConfigurer(SysUserFacade sysUserFacade, SecurityConfig securityConfig) {
		this.sysUserFacade = sysUserFacade;
		this.securityConfig = securityConfig;
	}

	@Override
	public void configure(B http) throws Exception {
		VipMemberAuthenticationFilter vipMemberAuthFilter = new VipMemberAuthenticationFilter();
		vipMemberAuthFilter.setAuthenticationManager(http.getSharedObject(AuthenticationManager.class));
		vipMemberAuthFilter.setSessionAuthenticationStrategy(new NullAuthenticatedSessionStrategy());
		// Login success processor
		vipMemberAuthFilter.setAuthenticationSuccessHandler(new VipMemberJsonLoginSuccessHandler(sysUserFacade, securityConfig));
		// Login failure processor
		vipMemberAuthFilter.setAuthenticationFailureHandler(new HttpStatusLoginFailureHandler());
		// Interceptor position
		VipMemberAuthenticationFilter vipMemberFilter = postProcess(vipMemberAuthFilter);
		http.addFilterAfter(vipMemberFilter, LogoutFilter.class);
	}
}
  • The main configuration class can generally inherit the WebSecurityConfigurerAdapter. In fact, all configurations are configured here. The filter configuration above should also be applied here. Note that the Provider defined by us should also be configured here. In fact, the authentication manager should be used for management. For example:
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
@Slf4j
public class WebSecurityConfig extends WebSecurityConfigurerAdapter{
	@Override
	protected void configure(HttpSecurity http) throws Exception {
		http.authorizeRequests()
				.antMatchers(securityConfig.getPermitUrls()).permitAll()
				.anyRequest().authenticated()
		        .and()
		    .csrf().disable()
		    .sessionManagement().disable()
		    .cors()
		    .and()
		    .headers().addHeaderWriter(new StaticHeadersWriter(Arrays.asList(
		    		new Header("Access-control-Allow-Origin","*"))))
		    .and()
		    .addFilterAfter(new OptionsRequestFilter(), CorsFilter.class)
		    .apply(new JsonLoginConfigurer<>(sysUserFacade, securityConfig))
		    .and()
		    .apply(new JwtLoginConfigurer<>(sysUserFacade, vipMemberFacade, securityConfig))
		    .and()
		    .logout()
		        .logoutSuccessHandler(new HttpStatusReturningLogoutSuccessHandler())
		    .and()
		    .sessionManagement().disable();
	}
}

Explanation of login process at source level

Process description

  • In fact, the whole authentication and authorization process is just a filter (chain). If an exception is thrown in the process, the exception handler will be called to return some error codes and error information to the front end. If there is no exception, the successful handler will be called to build a JWT authorization token to the front end.

Process description

  • Step 1: the user clicks the get SMS verification code interface to get the verification code. At the same time, the background stores the verification code in Redis.
  • Step 2: the user clicks login to initiate a login request. The request is blocked by VipMemberAuthenticationFilter, and the doFilter method of the filter will be called
  • The third step: the doFilter method calls the attemptAuthentication method, which will carry out the most basic check first, such as whether the mobile phone number is empty or the verification code is correct. After verification, it will build a VipMemberUsernamePasswordAuthenToken authentication and authentication asset, such as the mobile phone number, then call the AuthenticationManager authenticate method to continue. authentication.
  • Step 4: enter the authentication method of the AuthenticationManager. It will loop through all Providers to see which Provider supports the authentication of VipMemberUsernamePasswordAuthenToken. If supported, call the authentication of the corresponding Provider.
  • Step 5: after judgment, the authentication method of VipMemberJsonAuthenticationProvider will be called to authenticate the database layer, such as whether the user can be queried according to the mobile phone number, whether the queried user is disabled, etc.
  • Step 6: in the previous step, after the authentication is successful, the Token with the user information and the authenticate d Token will be returned. In the fourth step, the authentication method of the AuthenticationManager will be returned. After various judgments or processing, the system will continue to return to the doFilter in the third step, and then the successfulAuthentication method will be called. In the method, the user information will be saved to the SecurityContext, and then the authentication method will be called Use the success processor we defined.
  • Step 7: the main purpose of the successful processor VipMemberJsonLoginSuccessHandler is to build a token to the front end, and the whole login process is over.

JWT token introduction

  • Full name: Json Web Token
  • characteristic:
    • Self contained, that is, it contains some user information. Compared with the previous random string (JSSESSIONID), when the previous JSSESSIONID cache is lost, the login information is lost, and JWT can find the user data again through the information it contains.
    • Sign with salt to prevent others from modifying.
    • It is extensible. You can put any information you want, but you don't need to put sensitive information. As long as you have a token, you can view the information inside.

Solve some problems by looking at the source code

  • Create and configure a custom AuthenticationEventPublisher to implement some custom login event publishing.

Authorization control

  • According to different login roles, access to the interface is limited according to the role access rights.

Discuss a problem

  • We know that after the successful login, the user information will be saved to the SecurityContext. In fact, we can know that the content is really saved in the ThreadLocal local local thread variable. If the end of the request is unclear, the user information will always exist in that thread. If other users use this thread to initiate another request, they will get the information of the previous user So the question is: when is the user information in ThreadLocal cleared?

Posted by ChrisF79 on Sun, 21 Jun 2020 01:19:50 -0700