web-security第五期:使用Spring Security+JWT实现基于令牌的访问

源码地址:链接 (Spring-Security)

前两期分别分析了Spring Security Authentication 和 JWT,这一节组合这两个技术,完成 记住我的功能

1.令牌工具类

使用上一期的知识,很容易写一个下面的令牌操作工具类:

/**
 * 登录令牌操作
 *
 * @author swing
 */
public class JwtService {
    /**
     * 令牌有效期(30分钟)
     */
    private static final int EXPIRE_TIME = 1000 * 60 * 30;
    /**
     * 携带令牌信息的头
     */
    private static final String TOKEN_HEADER = "Authorization";
    /**
     * 令牌前缀
     */
    private static final String TOKEN_PREFIX = "Bearer ";
    /**
     * 密钥
     */
    private static final SecretKey SECRET_KEY = Keys.secretKeyFor(SignatureAlgorithm.HS256);

    /**
     * 创建令牌
     *
     * @param userDO 用户信息
     * @return 令牌
     */
    public static String createToken(UserDO userDO) {
        //设置令牌存储的信息内容
        Map<String, Object> claims = new HashMap<>(2);
        claims.put("username", userDO.getUsername());
        claims.put("password", userDO.getPassword());
        //创建令牌
        return Jwts
                .builder()
                .setClaims(claims)
                .setExpiration(new Date(System.currentTimeMillis() + EXPIRE_TIME))
                .signWith(SECRET_KEY)
                .compact();
    }

    /**
     * 解析token
     *
     * @param request 请求
     * @return token中的信息
     */
    public static Map<String, Object> resolverToken(HttpServletRequest request) {
        String token = request.getHeader(TOKEN_HEADER);
        if (token != null && token.startsWith(TOKEN_PREFIX)) {
            token = token.replace(TOKEN_PREFIX, "");
            //解析token
            return Jwts.parserBuilder()
                    .setSigningKey(SECRET_KEY)
                    .build()
                    .parseClaimsJws(token)
                    .getBody();
        }
        return null;
    }
}

2.登录

public class LoginService {
    @Resource
    private AuthenticationManager authenticationManager;

    /**
     * 登录认证
     *
     * @param userDO 用户信息
     * @return token
     */
    public String login(UserDO userDO) {
        Authentication authentication = authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(userDO.getUsername(), userDO.getPassword()));
        return JwtService.createToken(userDO);
    }
}
/**
     * 验证登录信息
     *
     * @return 登录结果
     */
    @PostMapping
    @ResponseBody
    RestResponse loginIn(@Validated @RequestBody UserDO userDO) {
        String token = loginService.login(userDO);
        Map<String, Object> body = new HashMap<>(1);
        body.put("token", token);
        return new RestResponse(HttpStatus.OK.value(), "认证成功!", body);
    }

我们将登录成功的用户信息(用户名和密码)存储在token内(这是入门例子,正式开发不建议这么做)

登录成功响应结果如下:

{
  "status": 200,
  "msg": "认证成功!",
  "body": {
    "token": "eyJhbGciOiJIUzI1NiJ9.eyJwYXNzd29yZCI6IjEyMzQ1NiIsInVzZXJuYW1lIjoic3dpbmciLCJleHAiOjE1OTE4NzEzNjl9.zVXr258NxQfS6KhYbnhA1pQHTn6fSNPmUaIV9K_ej9w"
  }
}

3.记住我

在第三期的内容中,我们知道用户名和密码的验证实在 UsernamePasswordAuthenticationFilter  过滤器开始的,认证的结果是向SecurityContextHolder中填充值(Authorities) 的过程,如果SecurityContextHolder中被填充了Authorities,那么此次请求就是被认证的请求,所以实现记住我的方法也很简单,我们只需要在 UsernamePasswordAuthenticationFilter 之前自定义一个过滤器进行token的验证,让后完成和UsernamePasswordAuthenticationFilter 同样的操作即可

过滤器代码如下:

/**
 * 验证该用户是否认证
 *
 * @author swing
 */
@Component
public class AuthenticationFilter extends OncePerRequestFilter {
    @Resource
    private AuthenticationManager authenticationManager;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        //清除之前遗留的认证信息
        SecurityContextHolder.clearContext();
        //获取token中的信息
        Map<String, Object> claims = JwtService.resolverToken(request);
        if (claims != null) {
            Authentication authentication = authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(claims.get("username"), claims.get("password")));
            SecurityContextHolder.getContext().setAuthentication(authentication);
        }
        filterChain.doFilter(request, response);
    }
}

然后在SecurityConfig中配置即可

protected void configure(HttpSecurity http) throws Exception {
        http
                .csrf().disable()
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
                .authorizeRequests()
                //允许匿名访问的api,其他的需要验证
                .antMatchers("/login", "/login/page").permitAll()
                // 除上面外的所有请求全部需要鉴权认证
                .anyRequest().authenticated();
        //将认证设置在UsernamePasswordAuthenticationFilter之前
        http.addFilterBefore(authenticationFilter, UsernamePasswordAuthenticationFilter.class);
    }

下次请求的时候带上我们生产的token,如下例:

GET http://localhost:8080/file/3
Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJwYXNzd29yZCI6IjEyMzQ1NiIsInVzZXJuYW1lIjoic3dpbmciLCJleHAiOjE1OTE4NzE4ODJ9.aUUz3hKle8FYNDW_aPlzHyeLIyO_JUfn27i2srORR9o

另外token是有过期时间点的,我们在创建令牌的时候声明了它,当 jjwt 在解析令牌的时候,会根据当前系统的时间来判断令牌是否过期,如果过期,则会抛出一个ExpiredJwtException,然后在web 层使用ExceptionHandler捕获即可

猜你喜欢

转载自blog.csdn.net/qq_42013035/article/details/106692246