源码地址:链接 (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捕获即可