Session認證與Token認證的取捨
在項目剛剛開始的時候,我還是規劃使用Session認證
的,期間遇到了不少問題。
- Session認證是通過把Cookie交給服務端管理的,而fetch在設置credentials時又會要求CORS的
Access-Control-Allow-Origin
不能設置爲*,必須指定域名。 - 前後端分離中,前端還是自行管理狀態纔是真正的前後端分離
- 後臺提供的RESTfulAPI,指定域名不符合RESTful的設計要求
所以最終我決定改用Token認證
,不過不使用OAuth2,而是自己編寫相關邏輯
Token的生成使用的是JWT
,減少請求時對數據庫的查詢
Token認證源碼解析
項目是使用Spring Security
作爲安全框架,對用戶進行認證、權限管理,所以要集成JWT
的話,有幾點要關注:
- 將存儲在Header中的
JWT
轉換爲Spring Security
的UserDetails
- 將必要的信息存儲在Token當中,返回給用戶
- 提供獲取、刷新Token的接口
所以就有了以下幾個類
TokenFilter
負責將請求中的Token轉換爲UsernamePasswordAuthenticationToken
// 有時請求會錯誤發送null與undefined
final String nullStr = "null";
final String undefinedStr = "undefined";
String token = getRequestToken(request);
// 如果token不存在,則直接放行,由之後的Filter攔截
if(StringUtils.isBlank(token) ||
nullStr.equals(token) ||
undefinedStr.equals(token)) {
chain.doFilter(request,response);
return;
}
// 從Token中讀取User,設置Authentication
SecurityUser user = securityService.getUserByToken(token);
if(user != null && SecurityUtils.getAuthentication() == null){
UsernamePasswordAuthenticationToken authenticationToken =
new UsernamePasswordAuthenticationToken(user,null,user.getAuthorities());
authenticationToken.setDetails(new WebAuthenticationDetails(request));
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
}
chain.doFilter(request,response);
SecurityService
負責提供Token相關的服務
- 登錄時,調用
Spring Security
的AuthenticationManager
,對用戶進行驗證,並在驗證通過時生成Token - Token中包裝了三段重要的信息:
id、username、role
,通過這些信息便足以組成一個用戶的基本對象 - 服務提供了從Token中獲取各類信息的方法,也提供了驗證Token是否合法、過期的方法,確保Token的可靠性
@Override
public AccessToken login(String username, String password) {
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
username, password);
HttpServletRequest request = ServletUtils.getRequest();
authRequest.setDetails(new WebAuthenticationDetails(request));
try{
Authentication authentication = authenticationManager.authenticate(authRequest);
SecurityUser user = (SecurityUser) authentication.getPrincipal();
return generateToken(user);
}catch (BadCredentialsException e){
throw new CommonException("用戶名/密碼不正確");
}catch (LockedException e){
throw new CommonException("賬號被鎖定,請聯繫管理員");
}
}
@Override
public AccessToken generateToken(SecurityUser user) {
if(user.getLocked()){
throw new CommonException("賬號被鎖定,請聯繫管理員");
}
AccessToken accessToken = new AccessToken();
long expire = jwtSettings.getExpire();
Date nowDate = new Date();
Date expireDate = new Date(nowDate.getTime() + expire * 1000);
// 構建JWT
String token = Jwts.builder()
// 將User的標識信息放入JWT中
.setSubject(String.valueOf(user.getId()))
.claim(CLAIM_USERNAME,user.getUsername())
.claim(CLAIM_ROLE,user.getRole())
// 設置過期時間
.setIssuedAt(nowDate)
.setExpiration(expireDate)
// 設置加密方式
.signWith(SignatureAlgorithm.HS512,
jwtSettings.getSecret())
.compact();
accessToken.setToken(token);
accessToken.setExpire(expire);
accessToken.setUser(user);
return accessToken;
}
AuthController
負責提供登錄以及刷新Token的接口
/**
* @param username 用戶名
* @param password 密碼
* @return AccessToken
*/
@ApiOperation(value = "登錄,獲取Token")
@RequestMapping(value = "/login",method = RequestMethod.POST)
public Result<AccessToken> login(@ApiParam("用戶名") @RequestParam String username,
@ApiParam("密碼") @RequestParam String password) {
AccessToken accessToken = securityService.login(username,password);
return Result.ok(accessToken);
}
/**
* 通過老Token換取新Token
* @param token 老Token
* @return 新Token
*/
@ApiOperation("通過老Token換取新Token")
@RequestMapping(value = "/refresh",method = RequestMethod.POST)
public Result<AccessToken> refresh(@ApiParam("老Token") @RequestParam String token){
Integer userId = securityService.getIDByToken(token);
UserDO user = userService.getByID(userId);
AccessToken accessToken = securityService.generateToken(new SecurityUser(user));
return Result.ok(accessToken);
}
至此,便完成了Spring Security
和JWT
的整合便完成了。