源碼地址:鏈接 (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捕獲即可