security整合JWT

7/27/2022 安全框架

# 前述

# SpringSecurity整合JWT实现无状态登录

# 引入依赖

<!-- JWT -->
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.9.1</version>
</dependency>
1
2
3
4
5
6

# yml配置

spring:
  redis:
    host: localhost
    port: 6379
    database: 0
    password:
    timeout: 10s
    lettuce:
      pool:
        min-idle: 0
        max-idle: 8
        max-active: 8
        max-wait: -1ms
  datasource:
    url: jdbc:mysql://127.0.0.1:3306/security-simple?useSSL=false&serverTimezone=Asia/Shanghai
    username: root
    password: admin
    type: com.alibaba.druid.pool.DruidDataSource
  thymeleaf:
    cache: false
  mvc:
    static-path-pattern: /static/**
#mybatis-plus 配置
mybatis-plus:
  mapper-locations: classpath:/mapper/**/*.xml
  type-aliases-package: com.example.jwt.project.sys.entity.*
  type-aliases-super-type: java.lang.Object
  global-config:
    db-config:
      id-type: auto
  configuration:
    aggressive-lazy-loading: false
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
token:
  # 令牌自定义标识
  header: Authorization
  # 令牌密钥
  secret: guhjdsagyuhj5678324hfygf
  # 令牌有效期(默认30分钟)
  expireTime: 30
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

# 新增TokenManager管理器

/**
 * @version 1.0.0
 * @className: TokenManager
 * @description: jwt令牌管理:
 * JWT实现大体思路:
 * 根据UUID生成一个JWT——token,此 token 并不会设置过期时间,只做数据的记录
 * (缓存前缀 + UUID) 作为 Redis 缓存对象的 key,同时 UUID 又是保存在 token 中的,所以 token 也就与 Redis关联了起来
 * 只有通过 token 获取到 UUID后,才能在 Redis 中获取到登录对象,如果没有 token,也就获取不到对象,则认证失败,否则成功
 * @author: LiJunYi
 * @create: 2022/7/27 11:16
 */
@Component
@Slf4j
public class TokenManager
{
    /**
     * 令牌自定义标识
     */
    @Value("${token.header}")
    private String header;

    /**
     * 令牌秘钥
     */
    @Value("${token.secret}")
    private String secret;

    /**
     * 令牌有效期(默认30分钟)
     */
    @Value("${token.expireTime}")
    private int expireTime;

    protected static final long MILLIS_SECOND = 1000;

    protected static final long MILLIS_MINUTE = 60 * MILLIS_SECOND;

    private static final Long MILLIS_MINUTE_TEN = 20 * 60 * 1000L;

    private final RedisCache redisCache;

    public TokenManager(RedisCache redisCache) {
        this.redisCache = redisCache;
    }


    /**
     * 从 request 中获取token
     * 通过解析token获取用户在redis中缓存的key从而获取到登录用户信息
     *
     * @param request 请求
     * @return {@link LoginUser}
     */
    public LoginUser getLoginUser(HttpServletRequest request)
    {
        // 获取请求携带的令牌
        String token = getToken(request);
        if (StrUtil.isNotEmpty(token))
        {
            try
            {
                Claims claims = parseToken(token);
                // 获取UUID
                String uuid = (String) claims.get(Constants.LOGIN_USER_KEY);
                // 获取 Redis 缓存中的 key
                String userKey = getTokenKey(uuid);
                return redisCache.getCacheObject(userKey);
            }
            catch (Exception ignored)
            {

            }
        }
        return null;
    }

    /**
     * 设置用户身份信息
     */
    public void setLoginUser(LoginUser loginUser)
    {
        if (ObjectUtil.isNotNull(loginUser) && StrUtil.isNotEmpty(loginUser.getToken()))
        {
            refreshToken(loginUser);
        }
    }

    /**
     * 删除令牌
     *
     * @param token 令牌
     */
    public void removeToken(String token)
    {
        if (StrUtil.isNotEmpty(token))
        {
            String userKey = getTokenKey(token);
            redisCache.deleteObject(userKey);
        }
    }

    /**
     * 创建令牌
     *
     * @param loginUser 登录用户
     * @return {@link String}
     */
    public String createToken(LoginUser loginUser)
    {
        String uuid = IdUtil.fastUUID();
        loginUser.setToken(uuid);
        refreshToken(loginUser);

        Map<String, Object> claims = new HashMap<>(8);
        claims.put(Constants.LOGIN_USER_KEY, uuid);
        return createToken(claims);
    }


    /**
     * 验证令牌有效期,自动刷新缓存
     *
     * @param loginUser 登录用户
     */
    public void verifyToken(LoginUser loginUser)
    {
        long expireTime = loginUser.getExpireTime();
        long currentTime = System.currentTimeMillis();
        if (expireTime - currentTime <= MILLIS_MINUTE_TEN)
        {
            refreshToken(loginUser);
        }
    }

    /**
     * 创建token时存入当前登录用户并同时刷新令牌过期时间
     *
     * @param loginUser 登录用户
     */
    public void refreshToken(LoginUser loginUser)
    {
        loginUser.setLoginTime(System.currentTimeMillis());
        loginUser.setExpireTime(loginUser.getLoginTime() + expireTime * MILLIS_MINUTE);
        // 根据uuid将loginUser缓存
        String userKey = getTokenKey(loginUser.getToken());
        redisCache.setCacheObject(userKey, loginUser, expireTime, TimeUnit.MINUTES);
    }

    /**
     * 创建令牌
     *
     * @param claims 数据声明
     * @return {@link String}
     */
    public String createToken(Map<String, Object> claims)
    {
        return Jwts.builder()
                .setClaims(claims)
                .signWith(SignatureAlgorithm.HS512, secret).compact();
    }

    /**
     * 从令牌中获取数据声明
     *
     * @param token 令牌
     * @return 数据声明
     */
    private Claims parseToken(String token)
    {
        return Jwts.parser()
                .setSigningKey(secret)
                .parseClaimsJws(token)
                .getBody();
    }

    /**
     * 获取令牌
     *
     * @param request 请求
     * @return {@link String}
     */
    private String getToken(HttpServletRequest request)
    {
        String token = request.getHeader(header);
        if (StrUtil.isNotEmpty(token) && token.startsWith(Constants.TOKEN_PREFIX))
        {
            token = token.replace(Constants.TOKEN_PREFIX, "");
        }
        return token;
    }

    /**
     * 获取令牌在缓存中的key
     *
     * @param uuid 随机UUID
     * @return {@link String}
     */
    private String getTokenKey(String uuid)
    {
        return Constants.LOGIN_TOKEN_KEY + uuid;
    }
}
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
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202

# 新增JWT自定义认证过滤器

/**
 * @version 1.0.0
 * @className: JwtAuthenticationTokenFilter
 * @description: JWT自定义认证过滤器
 * @author: LiJunYi
 * @create: 2022/7/27 11:54
 */
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter
{
    private final TokenManager tokenManager;


    public JwtAuthenticationTokenFilter(TokenManager tokenManager) {
        this.tokenManager = tokenManager;
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException {
        LoginUser loginUser = tokenManager.getLoginUser(request);
        if (ObjectUtil.isNotNull(loginUser) && ObjectUtil.isNull(SecurityUtils.getAuthentication()))
        {
            tokenManager.verifyToken(loginUser);
            UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginUser, null, loginUser.getAuthorities());
            authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
            SecurityContextHolder.getContext().setAuthentication(authenticationToken);
        }
        chain.doFilter(request,response);
    }
}
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

# 修改退出处理器

/**
 * @version 1.0.0
 * @className: LogoutSuccessHandlerImpl
 * @description: 退出处理器
 * @author: LiJunYi
 * @create: 2022/7/26 8:34
 */
@Configuration
public class LogoutSuccessHandlerImpl implements LogoutSuccessHandler
{
    private final TokenManager tokenManager;

    public LogoutSuccessHandlerImpl(TokenManager tokenManager)
    {
        this.tokenManager = tokenManager;
    }


    @Override
    public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication e) throws IOException {
        LoginUser loginUser = tokenManager.getLoginUser(request);
        if(ObjectUtil.isNotNull(loginUser))
        {
            //移除
            tokenManager.removeToken(loginUser.getToken());
        }
        ServletUtils.renderString(response, JSON.toJSONString(AjaxResult.success("退出成功")));
    }
}
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

# 修改SpringSecurity配置

/**
 * @version 1.0.0
 * @className: SecurityConfig
 * @description: SpringSecurity 5.7.x新用法配置
 * @author: LiJunYi
 * @create: 2022/7/26 8:43
 */
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class SecurityConfig
{
    /**
     * 自定义用户登录处理逻辑
     */
    private final MyUserDetailsServiceImpl userDetailsService;

    /**
     * 注销成功后处理器
     */
    private final LogoutSuccessHandlerImpl logoutSuccessHandler;

    /**
     * 无权限处理器
     */
    private final AuthenticationEntryPointImpl unauthorizedHandler;

    /**
     * 并发登录控制处理器
     */
    private final CustomExpiredSessionStrategyImpl expiredSessionStrategy;

    /**
     * jwt身份验证令牌过滤器
     */
    private final JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;

    /**
     * 安全配置
     * 构造函数注入
     *
     * @param tokenManager                 令牌管理器
     * @param redisCache                   缓存
     * @param userDetailsService           用户详细信息服务
     * @param logoutSuccessHandler         退出处理器
     * @param unauthorizedHandler          无权限处理器
     * @param expiredSessionStrategy       并发登录控制处理器
     * @param jwtAuthenticationTokenFilter JWT登录过滤器
     */
    @Autowired
    public SecurityConfig(TokenManager tokenManager, RedisCache redisCache, MyUserDetailsServiceImpl userDetailsService, LogoutSuccessHandlerImpl logoutSuccessHandler,
                          AuthenticationEntryPointImpl unauthorizedHandler,
                          CustomExpiredSessionStrategyImpl expiredSessionStrategy,
                          JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter) {
        this.jwtAuthenticationTokenFilter = jwtAuthenticationTokenFilter;
        this.userDetailsService = userDetailsService;
        this.logoutSuccessHandler = logoutSuccessHandler;
        this.unauthorizedHandler = unauthorizedHandler;
        this.expiredSessionStrategy = expiredSessionStrategy;
    }


    /**
     * 获取AuthenticationManager(认证管理器),登录时认证使用
     */
    @Bean
    public AuthenticationManager authenticationManager(AuthenticationConfiguration configuration) throws Exception {
        AuthenticationManager authenticationManager = configuration.getAuthenticationManager();
        return configuration.getAuthenticationManager();
    }

    /**
     * 配置加密方式
     *
     * @return {@link PasswordEncoder}
     */
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    /**
     * 侦听器配置,使 Spring Security 更新有关会话生命周期事件的信息
     * 并发会话控制:https://docs.spring.io/spring-security/reference/servlet/authentication/session-management.html
     * @return {@link HttpSessionEventPublisher}
     */
    @Bean
    public HttpSessionEventPublisher httpSessionEventPublisher() {
        return new HttpSessionEventPublisher();
    }

    /**
     * 过滤器链
     *
     * @param http http
     * @return {@link SecurityFilterChain}
     * @throws Exception 异常
     */
    @Bean
    SecurityFilterChain filterChain(HttpSecurity http) throws Exception
    {
        http.authorizeRequests(authorize ->
                authorize.mvcMatchers("/userLogin","/noPermission").permitAll()
                        .anyRequest().authenticated()
        )
                .csrf().disable()
                // 基于token,不需要session
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .headers().frameOptions().disable();
        // 退出过滤器
        http.logout(logout -> logout
                .logoutUrl("/logout")
                .deleteCookies("JSESSIONID")
                .logoutSuccessHandler(logoutSuccessHandler));
        // 添加JWT filter
        http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
        // by default uses a Bean by the name of corsConfigurationSource
        http.cors(withDefaults());
        // 无权限处理器
        http.exceptionHandling().authenticationEntryPoint(unauthorizedHandler).accessDeniedPage("/noPermission");
        // 并发会话控制
        http.sessionManagement(session ->  session
                .maximumSessions(1)
                .expiredSessionStrategy(expiredSessionStrategy));
        http.userDetailsService(userDetailsService);
        return http.build();
    }

    /**
     * 配置跨源访问(CORS)
     * 官方文档:https://docs.spring.io/spring-security/reference/servlet/integrations/cors.html
     * @return {@link CorsConfigurationSource}
     */
    @Bean
    CorsConfigurationSource corsConfigurationSource()
    {
        CorsConfiguration configuration = new CorsConfiguration();
        configuration.setAllowedOrigins(Arrays.asList("http://localhost:8080/","https://example.com"));
        configuration.setAllowedMethods(Arrays.asList("GET","POST"));
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", configuration);
        return source;
    }
}
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
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145

# 修改登录接口

/**
 * 登录控制器
 *
 * @author LiJunYi
 * @date 2022/07/26
 */
@RestController
public class LoginController
{
    @Autowired
    ISysUserService userService;

    @Autowired
    SysLoginService loginService;

    /**
     * 登录方法
     *
     * @param user 用户
     * @return {@link AjaxResult}
     */
    @PostMapping("userLogin")
    public AjaxResult login(SysUser user)
    {
        if (StrUtil.isEmpty(user.getUsername()) || StrUtil.isEmpty(user.getPassword()))
        {
            return AjaxResult.error("用户名或密码未输入!");
        }
        String token = loginService.login(user.getUsername(),user.getPassword());
        return AjaxResult.success(Constants.TOKEN, token);
    }
}
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

# 登录业务逻辑代码

/**
 * @version 1.0.0
 * @className: SysLoginService
 * @description: 登录业务
 * @author: LiJunYi
 * @create: 2022/7/27 12:53
 */
@Component
public class SysLoginService
{
    @Autowired
    private TokenManager tokenManager;

    @Resource
    private AuthenticationManager authenticationManager;

    @Autowired
    private RedisCache redisCache;

    @Autowired
    private ISysUserService userService;

    /**
     * 登录验证
     *
     * @param username 用户名
     * @param password 密码
     * @return 结果
     */
    public String login(String username, String password)
    {
        // 用户验证
        Authentication authentication;
        try
        {
            // 该方法会去调用UserDetailsServiceImpl.loadUserByUsername
            authentication = authenticationManager
                    .authenticate(new UsernamePasswordAuthenticationToken(username, password));
        }
        catch (Exception e)
        {
            if (e instanceof BadCredentialsException)
            {
                throw new ServiceException("用户名密码错误");
            }
            else
            {
                throw new ServiceException(e.getMessage());
            }
        }
        LoginUser loginUser = (LoginUser) authentication.getPrincipal();
        // 生成token
        return tokenManager.createToken(loginUser);
    }
}
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

# 测试效果图

SpringSecurity

SpringSecurity

SpringSecurity

SpringSecurity

SpringSecurity