• SpringBoot + Security + JWT安全策略



    security进行用户验证和授权;jwt负责颁发令牌与校验,判断用户登录状态

    一、原理

    1. SpringSecurity 过滤器链

    在这里插入图片描述
    SpringSecurity 采用的是责任链的设计模式,它有一条很长的过滤器链。

    • SecurityContextPersistenceFilter:在每次请求处理之前将该请求相关的安全上下文信息加载到 SecurityContextHolder 中。
    • LogoutFilter:用于处理退出登录。
    • UsernamePasswordAuthenticationFilter:用于处理基于表单的登录请求,从表单中获取用户名和密码。
    • BasicAuthenticationFilter:检测和处理 http basic 认证。
    • ExceptionTranslationFilter:处理 AccessDeniedException 和 AuthenticationException 异常。
    • FilterSecurityInterceptor:可以看做过滤器链的出口。

    流程说明:客户端发起一个请求,进入 Security 过滤器链。
    当到 LogoutFilter 的时候判断是否是登出路径,如果是登出路径则到 logoutHandler ,如果登出成功则到 logoutSuccessHandler 登出成功处理,如果登出失败则由 ExceptionTranslationFilter ;如果不是登出路径则直接进入下一个过滤器。

    当到 UsernamePasswordAuthenticationFilter 的时候判断是否为登录路径,如果是,则进入该过滤器进行登录操作,如果登录失败则到 AuthenticationFailureHandler 登录失败处理器处理,如果登录成功则到 AuthenticationSuccessHandler 登录成功处理器处理,如果不是登录请求则不进入该过滤器。

    当到 FilterSecurityInterceptor 的时候会拿到 uri ,根据 uri 去找对应的鉴权管理器,鉴权管理器做鉴权工作,鉴权成功则到 Controller 层否则到 AccessDeniedHandler 鉴权失败处理器处理。

    2. JWT校验

    在这里插入图片描述
    首先前端一样是把登录信息发送给后端,后端查询数据库校验用户的账号和密码是否正确,正确的话则使用jwt生成token,并且返回给前端。以后前端每次请求时,都需要携带token,后端获取token后,使用jwt进行验证用户的token是否无效或过期,验证成功后才去做相应的逻辑。

    二、Security + JWT配置说明

    1. 添加maven依赖

    <dependency>
        <groupId>org.springframework.bootgroupId>
        <artifactId>spring-boot-starter-securityartifactId>
    dependency>
    
    <dependency>
        <groupId>io.jsonwebtokengroupId>
        <artifactId>jjwtartifactId>
        <version>0.9.1version>
    dependency>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    2. securityConfig配置

    /**
     * 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);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97

    3. JwtAuthenticationFilter 校验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;
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122

    4.JWT生成与解析工具类

    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());
        }
    
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
  • 相关阅读:
    jmeter接口自动化
    Alad de Qnget
    elasticsearch 7.X全部版本的新特性与重大变化
    class与struct
    「Django秘境探险:揭开Web开发的神秘面纱」
    SQL命令及MariaDB(二)
    Python 批量修改文件名
    【C# 7.0 in a Nutshell】第4章 C#的高级特性——委托
    Mac如何打开企业微信内置浏览器控制台
    window下VS2022封装动态库以及调用动态库
  • 原文地址:https://blog.csdn.net/weixin_45698637/article/details/127444635