第12章 Spring Security JWT登录

什么是JWT

JWS 是 JSON Web Token 的缩写,用JSON作为对象在系统之间安全地传输信息。关于JWT更多信息,请参考阮一峰的 JSON Web Token 入门教程

注意:本章是基于第三章 Spring Security基于数据库登录实现的

Maven依赖
<!-- 新增redis依赖 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- 新增jwt依赖 -->
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-api</artifactId>
    <version>0.10.7</version>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-impl</artifactId>
    <version>0.10.7</version>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-jackson</artifactId>
    <version>0.10.7</version>
</dependency>
新增JWT工具类
public class JwtTokenUtils {

    public static final String JWT_SECRET_KEY = "C*F-JaNdRgUkXn2r5u8x/A?D(G+KbPeShVmYq3s6v9y$B&E)H@McQfTjWnZr4u7w";
    private static final byte[] API_KEY_SECRET_BYTES = DatatypeConverter.parseBase64Binary(JWT_SECRET_KEY);
    // 秘钥
    private static final SecretKey SECRET_KEY = Keys.hmacShaKeyFor(API_KEY_SECRET_BYTES);

    private static final long EXPIRATION = 20 * 1000;
    public static final String TOKEN_HEADER = "Authorization";
    public static final String TOKEN_PREFIX = "Bearer ";
    public static final String TOKEN_TYPE = "JWT";
    public static final String ROLE_CLAIMS = "rol";

    public static String createToken(String username, Integer id, List<String> roles) {

        Date now = new Date();
        Date expirationDate = new Date(now.getTime() + EXPIRATION);
        String tokenPrefix = Jwts.builder()
                .setHeaderParam("type", TOKEN_TYPE)
                .signWith(SECRET_KEY, SignatureAlgorithm.HS256)
                .claim(ROLE_CLAIMS, String.join(",", roles))
                .setId(id.toString())
                .setIssuer("butterflyzh")
                .setIssuedAt(now)
                .setSubject(username)
                .setExpiration(expirationDate)
                .compact();
        return TOKEN_PREFIX + tokenPrefix;
    }

    public static UsernamePasswordAuthenticationToken getAuthentication(String token) {
        Claims claims = getClaims(token);
        List<SimpleGrantedAuthority> authorities = getAuthorities(claims);
        String username = claims.getSubject();
        return new UsernamePasswordAuthenticationToken(username, token, authorities);
    }

    private static List<SimpleGrantedAuthority> getAuthorities(Claims claims) {
        String roles = (String) claims.get(ROLE_CLAIMS);
        return Arrays.stream(roles.split(","))
                .map(SimpleGrantedAuthority::new)
                .collect(Collectors.toList());
    }

    public static String getId(String token) {
        Claims claims = getClaims(token);
        return claims.getSubject() + ":" + claims.getId();
    }

    private static Claims getClaims(String token) {
        return Jwts.parser().setSigningKey(SECRET_KEY)
                .parseClaimsJws(token)
                .getBody();
    }
}
RedisConfig配置
@Configuration
public class RedisConfig {
    @Bean
    public RedisTemplate<String, String> redisTemplate(RedisConnectionFactory factory) {
        RedisTemplate<String, String> redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(factory);
        return redisTemplate;
    }
}
新增Jwt过滤器链

这里我们需要在 UsernamePasswordAuthenticationFilter 过滤器之前新增 JwtAuthenticationFilter 过滤器链。该过滤器主要工作:解析请求头中 Authorization 字段,然后从Redis中取出服务器存储token,最后比对请求携带的token和服务器存储的token是否一样。

@Slf4j
public class JwtAuthenticationFilter extends OncePerRequestFilter {

    private final StringRedisTemplate redisTemplate;
    private final RequestMatcher requestMatcher = new AntPathRequestMatcher("/login");

    public JwtAuthenticationFilter(StringRedisTemplate redisTemplate) {
        this.redisTemplate = redisTemplate;
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        // /login登录地址不应该拦截
        if (requestMatcher.matches(request)) {
            filterChain.doFilter(request, response);
            return;
        }
        String token = request.getHeader(JwtTokenUtils.TOKEN_HEADER);
        if (token == null || !token.startsWith(JwtTokenUtils.TOKEN_PREFIX)) {
            SecurityContextHolder.clearContext();
            filterChain.doFilter(request, response);
            return;
        }
        String tokenValue = token.replace(JwtTokenUtils.TOKEN_PREFIX, "");
        UsernamePasswordAuthenticationToken authentication = null;

        try {
            String oldToken = redisTemplate.opsForValue().get(JwtTokenUtils.getId(tokenValue));
            if (!token.equals(oldToken)) {
                SecurityContextHolder.clearContext();
                filterChain.doFilter(request, response);
                return;
            }
            authentication = JwtTokenUtils.getAuthentication(tokenValue);
        } catch (JwtException e) {
            logger.error("Invalid jwt : " + e);
        }
        SecurityContextHolder.getContext().setAuthentication(authentication);
        filterChain.doFilter(request, response);
    }
}
自定义AuthenticationSuccessHandler

分析一下,我们需要在哪里地方去生成 token 呢?我觉得在登录之后生成比较好。之前分析登录流程时,登录成功时,过滤器 AbstractAuthenticationProcessingFilter 会调用 AuthenticationSuccessHandler 处理器进行重定向。那现在我们可以自定义处理器,在完成登录时,生成 token 并存储到redis中。

public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler {

    private StringRedisTemplate redisTemplate;

    public MyAuthenticationSuccessHandler(StringRedisTemplate redisTemplate) {
        this.redisTemplate = redisTemplate;
    }

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

        User user = (User) authentication.getPrincipal();
        String username = user.getUsername();
        List<String> authorities = user.getAuthorities().stream().map(GrantedAuthority::getAuthority).collect(Collectors.toList());
        String token = JwtTokenUtils.createToken(username, user.getId(), authorities);
        redisTemplate.opsForValue().set(user.getId().toString(), token);

        httpServletResponse.setHeader(JwtTokenUtils.TOKEN_HEADER, token);
    }
}
WebSecurityConfig配置

这里我修改 configure(HttpSecurity http) 方法。这里我们需要配置 sessionCreationPolicy(SessionCreationPolicy.STATELESS) 创建策略,因为使用jwt作为token,就不需要session保持状态。

@Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .authorizeRequests()
                .anyRequest().authenticated()
                .and()
                .formLogin()
                .successHandler(new MyAuthenticationSuccessHandler(redisTemplate))
                .and()
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .csrf().disable();
        http.addFilterBefore(new JwtAuthenticationFilter(redisTemplate), UsernamePasswordAuthenticationFilter.class);
    }
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值