一、什么是JWT
說起JWT,我們應該來談一談基於token的認證和傳統的session認證的區別。說起JWT,我們應該來談一談基於token的認證和傳統的session認證的區別。
(1)、session所存在的問題
Session: 每個用戶經過我們的應用認證之后,我們的應用都要在服務端做一次記錄,以方便用戶下次請求的鑒別,通常而言session都是保存在內存中,而隨着認證用戶的增多,服務端的開銷會明顯增大。
擴展性: 用戶認證之后,服務端做認證記錄,如果認證的記錄被保存在內存中的話,這意味着用戶下次請求還必須要請求在這台服務器上,這樣才能拿到授權的資源,這樣在分布式的應用上,相應的限制了負載均衡器的能力。這也意味着限制了應用的擴展能力。
CSRF: 因為是基於cookie來進行用戶識別的, cookie如果被截獲,用戶就會很容易受到跨站請求偽造的攻擊。
(2)、Token的鑒權機制
基於token的鑒權機制類似於http協議也是無狀態的,也就是說token認證機制的應用不需要去考慮用戶在哪一台服務器登錄了。
(3)、認識Token
JWT是由三段信息構成的,以 點(.) 分割,每部分都有不同的含義(每段都是用 Base64 編碼的)
第一部分為 頭部(Header)
第二部分為 載荷(Payload)
第三部分為 簽證(Signature)
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJhdWQiOiIxIiwiZXhwIjoxNjI1NDY3MDY5LCJ1c2VyTmFtZSI6IumBlW_mCIsImlhdCI6MTYyNTQ2NTI2OX0.e_uuksv0b8gqX9HUVEiieLQlKFKcLdxCxovJ3xA3wB8
第一部分通過Base64解碼出的結果是
{
"typ":"JWT",
"alg":"HS256"
}
由此可以得知jwt的頭部承載兩部分信息 類型和加密算法
第二部分是用來放主要的存儲信息的(主要信息中除了自定義信息還有標准中注冊的聲明)
iss: jwt簽發者
sub: jwt所面向的用戶
aud: 接收jwt的一方
exp: jwt的過期時間,這個過期時間必須要大於簽發時間
nbf: 定義在什么時間之前,該jwt都是不可用的.
iat: jwt的簽發時間
jti: jwt的唯一身份標識,主要用來作為一次性token,從而回避重放攻擊。
當然以上是統一標准而已,並非必須用,建議不強制。
第三部分需要base64加密后的header和base64加密后的payload使用.連接組成的字符串,然后通過header中聲明的加密方式進行加鹽secret組合加密,然后就構成了jwt的第三部分。
二、使用JWT
(1)、導入依賴
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.10.3</version>
</dependency>
(2)、創建JwtUtils工具類
@Value("{Jwt.secret}")
private static String secret;
/**
簽發對象:隨意
簽發時間:現在
有效時間:30分鍾
載荷內容:自定義內容
加密密鑰:鹽 + 密鑰
*/
public static String createToken(String userId,String userName) {
Calendar nowTime = Calendar.getInstance();
nowTime.add(Calendar.MINUTE,30);
Date expiresDate = nowTime.getTime();
//簽發對象
return JWT.create().withAudience(userId)
//發行時間
.withIssuedAt(new Date())
//有效時間
.withExpiresAt(expiresDate)
//載荷,隨便寫幾個都可以,也可以理解為自定義參數
.withClaim("userName", userName)
//加密
.sign(Algorithm.HMAC256(secret+"你隨意寫"));
}
/**
* 檢驗合法性,其中secret參數就應該傳入的是用戶的id
* @param token
*/
public static void verifyToken(String token){
DecodedJWT jwt = null;
try {
JWTVerifier verifier = JWT.require(Algorithm.HMAC256(secret+"WDNMD")).build();
jwt = verifier.verify(token);
} catch (Exception e) {
//效驗失敗
//這里拋出的異常是我自定義的一個異常,你也可以寫成別的
}
}
/**
* 獲取簽發對象
*/
public static String getAudience(String token) {
String audience = null;
try {
audience = JWT.decode(token).getAudience().get(0);
} catch (JWTDecodeException j) {
//這里是token解析失敗
}
return audience;
}
/**
* 通過載荷名字獲取載荷的值
*/
public static Claim getClaimByName(String token, String name){
return JWT.decode(token).getClaim(name);
}
三、JWT結合SpringSecurity實現登錄鑒權以及權限管理
(1)、思路
登陸成功返回Token,並把Token存儲到Redis中確保單點登錄。使用過濾器校驗Token和權限
(2)、SpringSecurity配置
由於使用Token進行登錄鑒權,就不需要Session了,因此需禁用Session
@Component
@EnableWebSecurity
/**
* 開啟@EnableGlobalMethodSecurity(prePostEnabled = true)注解
* 在繼承 WebSecurityConfigurerAdapter 這個類的類上面貼上這個注解
* 並且prePostEnabled設置為true,@PreAuthorize這個注解才能生效
* SpringSecurity默認是關閉注解功能的.
*/
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
//注入過濾器
@Resource
private JwtVerificationFilter jwtVerificationFilter;
@Override
protected void configure(HttpSecurity http) throws Exception {
//關閉csrf防護 >只有關閉了,才能接受來自表單的請求
http.csrf().disable()
.cors()//開啟跨域
.and()
//開啟授權請求
.authorizeRequests()
//放行接口,因為使用自定義登錄頁面所以需要放行
.antMatchers("/login/**").permitAll()
//攔截所有請求,所有請求都需要登錄認證
.anyRequest().authenticated()
.and()
.addFilterAfter(jwtVerificationFilter, UsernamePasswordAuthenticationFilter.class)
//前后端分離采用JWT 不需要session(添加后Spring永遠不會創建session)
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
}
}
(3)、編寫過濾器
/**
* @author admin
* 過濾器 發起請求前檢驗Token 實現並在每次請求時只執行一次過濾
* 在spring中,filter都默認繼承OncePerRequestFilter
* OncePerRequestFilter顧名思義,他能夠確保在一次請求只通過一次filter,而不需要重復執行
* 為了兼容不同的web container,特意而為之
*
* 在servlet2.3中,Filter會經過一切請求,包括服務器內部使用的forward轉發請求和<%@ include file=”/login.jsp”%>的情況
*
* servlet2.4中的Filter默認情況下只過濾外部提交的請求,forward和include這些內部轉發都不會被過濾,
*/
@Component
@Slf4j
public class JwtVerificationFilter extends OncePerRequestFilter {
@Resource
private RoleService roleService;
@Resource
private PermissionService permissionService;
@Resource
private RolePermissionService rolePermissionService;
/**
* 過濾器,檢驗Token
* 發起請求時會調用兩次,第二次是展示/favicon.ico
*
*/
@Override
protected void doFilterInternal(HttpServletRequest httpServletRequest, @NotNull HttpServletResponse httpServletResponse, @NotNull FilterChain filterChain) throws ServletException, IOException {
//獲取Token
String token = httpServletRequest.getHeader("token");
//非空校驗
if (token == null) {
filterChain.doFilter(httpServletRequest, httpServletResponse);
return;
}
//檢驗Token合法性
JwtUtils.getAudience(token);
//比對Redis中存儲的Token
String redisToken = RedisUtils.get(RedisPrefixKey.LOGIN_TOKEN.keyAppend(JwtUtils.getAudience(token)).getKey())
.toString();
if (!redisToken.equals(token)) {
filterChain.doFilter(httpServletRequest, httpServletResponse);
return;
}
//獲取權限 根據Token獲取載荷的值
List<GrantedAuthority> authorityList = this.findAllAuthority(Long.valueOf(JwtUtils.getAudience(token)));
//安全上下文,存儲認證授權的相關信息,實際上就是存儲"當前用戶"賬號信息和相關權限
SecurityContextHolder
.getContext()
.setAuthentication(new UsernamePasswordAuthenticationToken(null,null,authorityList));
//將請求轉發給過濾器鏈下一個filter
filterChain.doFilter(httpServletRequest, httpServletResponse);
}
/**
* 查找權限
*/
private List<GrantedAuthority> findAllAuthority(Long userId){
//1、拿到用戶的角色和權限
//2、返回的權限
List<GrantedAuthority> authorityList = new ArrayList<>();
//3、查出權限列表循環放入 authorityList 中
for (權限實體類 url : 權限集合) {
SimpleGrantedAuthority simpleGrantedAuthority = new SimpleGrantedAuthority(權限的url);
authorityList.add(simpleGrantedAuthority);
}
return authorityList;
}
}