SpringBoot + Security + JWT security policy


Security performs user verification and authorization; jwt is responsible for issuing tokens and verification, and judging user login status

1. Principle

1. Spring Security filter chain

insert image description here
Spring Security adopts the chain of responsibility design pattern, which has a long filter chain.

  • SecurityContextPersistenceFilter: Before each request is processed, the security context information related to the request is loaded into the SecurityContextHolder.
  • LogoutFilter: Used to handle logout.
  • UsernamePasswordAuthenticationFilter: Used to process form-based login requests and obtain usernames and passwords from the form.
  • BasicAuthenticationFilter: Detect and handle http basic authentication.
  • ExceptionTranslationFilter: Handles AccessDeniedException and AuthenticationException exceptions.
  • FilterSecurityInterceptor: It can be regarded as the exit of the filter chain.

Process Description: The client initiates a request and enters the Security filter chain.
When going to LogoutFilter, judge whether it is a logout path. If it is a logout path, go to logoutHandler. If the logout is successful, go to logoutSuccessHandler. If the logout fails, use ExceptionTranslationFilter; if it is not a logout path, go directly to the next a filter.

When going to UsernamePasswordAuthenticationFilter, judge whether it is a login path. If yes, enter the filter for login operation. If the login fails, go to AuthenticationFailureHandler for login failure processing. If login is successful, go to AuthenticationSuccessHandler for login success processing. If it is not login Requests do not enter this filter.

When going to FilterSecurityInterceptor, you will get the uri, and find the corresponding authentication manager according to the uri. The authentication manager will do the authentication work. If the authentication is successful, it will go to the Controller layer; otherwise, it will go to the AccessDeniedHandler authentication failure processor for processing.

2. JWT verification

insert image description here
First of all, the front-end also sends the login information to the back-end, and the back-end queries the database to verify whether the user's account and password are correct. If it is correct, it uses jwt to generate a token and returns it to the front-end. In the future, each time the front-end requests, it needs to carry the token. After the back-end obtains the token, it uses jwt to verify whether the user's token is invalid or expired. After the verification is successful, it does the corresponding logic.

2. Security + JWT configuration instructions

1. Add maven dependency

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>

<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.9.1</version>
</dependency>

2. securityConfig configuration

/**
 * Security 配置
 */
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    
    

    @Autowired
    LoginFailureHandler loginFailureHandler;

    @Autowired
    LoginSuccessHandler loginSuccessHandler;

    @Autowired
    JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint;

    @Autowired
    JwtAccessDeniedHandler jwtAccessDeniedHandler;

    @Autowired
    UserDetailServiceImpl userDetailService;

    @Autowired
    JWTLogoutSuccessHandler jwtLogoutSuccessHandler;

    @Autowired
    CaptchaFilter captchaFilter;

    @Value("${security.enable}")
    private Boolean securityIs = Boolean.TRUE;

    @Value("${security.permit}")
    private String permit;

    @Bean
    public HttpFirewall allowUrlEncodedSlashHttpFirewall() {
    
    
        StrictHttpFirewall firewall = new StrictHttpFirewall();
        //此处可添加别的规则,目前只设置 允许双 //
        firewall.setAllowUrlEncodedDoubleSlash(true);
        return firewall;
    }

    @Bean
    JwtAuthenticationFilter jwtAuthenticationFilter() throws Exception {
    
    
        JwtAuthenticationFilter jwtAuthenticationFilter = new JwtAuthenticationFilter(authenticationManager(), jwtAuthenticationEntryPoint);
        return jwtAuthenticationFilter;
    }

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

    @Override
    protected void configure(HttpSecurity http) throws Exception {
    
    
        ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry registry = http.cors().and().csrf().disable()
                // 登录配置
                .formLogin()
                .successHandler(loginSuccessHandler)
                .failureHandler(loginFailureHandler)

                .and()
                .logout()
                .logoutSuccessHandler(jwtLogoutSuccessHandler)

                // 禁用session
                .and()
                .sessionManagement()
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                // 配置拦截规则
                .and()
                .authorizeRequests()
                .antMatchers(permit.split(",")).permitAll();
        if (!securityIs) {
    
    
            http.authorizeRequests().antMatchers("/**").permitAll();
        }
        registry.anyRequest().authenticated()
                // 异常处理器
                .and()
                .exceptionHandling()
                .authenticationEntryPoint(jwtAuthenticationEntryPoint)
                .accessDeniedHandler(jwtAccessDeniedHandler)

                // 配置自定义的过滤器
                .and()
                .addFilter(jwtAuthenticationFilter())
                // 验证码过滤器放在UsernamePassword过滤器之前
                .addFilterBefore(captchaFilter, UsernamePasswordAuthenticationFilter.class);
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    
    
        auth.userDetailsService(userDetailService);
    }
}

3. JwtAuthenticationFilter check token

package cn.piesat.gf.filter;

import cn.hutool.core.exceptions.ExceptionUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONUtil;
import cn.piesat.gf.dao.user.SysUserDao;
import cn.piesat.gf.model.entity.user.SysUser;
import cn.piesat.gf.exception.ExpiredAuthenticationException;
import cn.piesat.gf.exception.MyAuthenticationException;
import cn.piesat.gf.service.user.impl.UserDetailServiceImpl;
import cn.piesat.gf.utils.Constants;
import cn.piesat.gf.utils.JwtUtils;
import cn.piesat.gf.utils.Result;
import io.jsonwebtoken.Claims;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
import org.springframework.util.Assert;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;

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

@Slf4j
public class JwtAuthenticationFilter extends BasicAuthenticationFilter {
    
    

    private AuthenticationEntryPoint authenticationEntryPoint;

    private AuthenticationManager authenticationManager;

    @Autowired
    JwtUtils jwtUtils;

    @Autowired
    UserDetailServiceImpl userDetailService;

    @Autowired
    SysUserDao sysUserRepository;

    @Autowired
    RedisTemplate redisTemplate;

    @Value("${security.single}")
    private Boolean singleLogin = false;

    public JwtAuthenticationFilter(AuthenticationManager authenticationManager, AuthenticationEntryPoint authenticationEntryPoint) {
    
    
        super(authenticationManager, authenticationEntryPoint);
        Assert.notNull(authenticationManager, "authenticationManager cannot be null");
        Assert.notNull(authenticationEntryPoint, "authenticationEntryPoint cannot be null");
        this.authenticationManager = authenticationManager;
        this.authenticationEntryPoint = authenticationEntryPoint;
    }

    public JwtAuthenticationFilter(AuthenticationManager authenticationManager) {
    
    
        super(authenticationManager);
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
    
    
        String jwt = request.getHeader(jwtUtils.getHeader());
        // 这里如果没有jwt,继续往后走,因为后面还有鉴权管理器等去判断是否拥有身份凭证,所以是可以放行的
        // 没有jwt相当于匿名访问,若有一些接口是需要权限的,则不能访问这些接口
        if (StrUtil.isBlankOrUndefined(jwt)) {
    
    
            chain.doFilter(request, response);
            return;
        }
        try {
    
    
            Claims claim = jwtUtils.getClaimsByToken(jwt);
            if (claim == null) {
    
    
                throw new MyAuthenticationException("token 异常");
            }
            if (jwtUtils.isTokenExpired(claim)) {
    
    
                throw new MyAuthenticationException("token 已过期");
            }
            String username = claim.getSubject();

            Object o1 = redisTemplate.opsForValue().get(Constants.TOKEN_KEY + username);
            String o = null;
            if(!ObjectUtils.isEmpty(o1)){
    
    
                o = o1.toString();
            }

            if (!StringUtils.hasText(o)) {
    
    
                throw new ExpiredAuthenticationException("您的登录信息已过期,请重新登录!");
            }
            if (singleLogin && StringUtils.hasText(o) && !jwt.equals(o)) {
    
    
                throw new MyAuthenticationException("您的账号已别处登录,您已下线,如有异常请及时修改密码!");
            }

            // 获取用户的权限等信息
            SysUser sysUser = sysUserRepository.findByUserName(username);

            // 构建UsernamePasswordAuthenticationToken,这里密码为null,是因为提供了正确的JWT,实现自动登录
            UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(username, null, userDetailService.getUserAuthority(sysUser.getUserId()));
            SecurityContextHolder.getContext().setAuthentication(token);

            chain.doFilter(request, response);
        } catch (AuthenticationException e) {
    
    
            log.error(ExceptionUtil.stacktraceToString(e));
            authenticationEntryPoint.commence(request, response, e);
            return;
        } catch (Exception e){
    
    
            log.error(ExceptionUtil.stacktraceToString(e));
            response.getOutputStream().write(JSONUtil.toJsonStr(Result.fail(e.getMessage())).getBytes(StandardCharsets.UTF_8));
            response.getOutputStream().flush();
            response.getOutputStream().close();
            return;
        }
    }
}

4. JWT generation and parsing tools

package cn.piesat.gf.utils;

import cn.hutool.core.exceptions.ExceptionUtil;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

import java.util.Date;

@Data
@Component
@ConfigurationProperties(prefix = "jwt.config")
@Slf4j
public class JwtUtils {
    
    

    private long expire;
    private String secret;
    private String header;

    // 生成JWT
    public String generateToken(String username) {
    
    

        Date nowDate = new Date();
        Date expireDate = new Date(nowDate.getTime() + 1000 * expire);

        return Jwts.builder()
                .setHeaderParam("typ", "JWT")
                .setSubject(username)
                .setIssuedAt(nowDate)
                .setExpiration(expireDate)
                .signWith(SignatureAlgorithm.HS512, secret)
                .compact();
    }

    // 解析JWT
    public Claims getClaimsByToken(String jwt) {
    
    
        try {
    
    
            return Jwts.parser()
                    .setSigningKey(secret)
                    .parseClaimsJws(jwt)
                    .getBody();
        } catch (Exception e) {
    
    
            log.error(ExceptionUtil.stacktraceToString(e));
            return null;
        }
    }

    // 判断JWT是否过期
    public boolean isTokenExpired(Claims claims) {
    
    
        return claims.getExpiration().before(new Date());
    }

}

Guess you like

Origin blog.csdn.net/weixin_45698637/article/details/127444635