在上一篇基礎上繼續集成 JWT ,實現用戶身份驗證。
前言
前后端分離項目中,如果直接把 API 接口對外開放,我們知道這樣風險是很大的,所以在上一篇中我們引入了 Spring Security ,但是我們在登陸后缺少了請求憑證部分。
什么是JWT?
JWT是 Json Web Token 的縮寫。它是基於 RFC 7519 標准定義的一種可以安全傳輸的 小巧 和 自包含 的JSON對象。由於數據是使用數字簽名的,所以是可信任的和安全的。JWT可以使用HMAC算法對secret進行加密或者使用RSA的公鑰私鑰對來進行簽名。
JWT的工作流程
1、用戶進入登錄頁,輸入用戶名、密碼,進行登錄;
2、服務器驗證登錄鑒權,如果改用戶合法,根據用戶的信息和服務器的規則生成 JWT Token
3、服務器將該 token 以 json 形式返回(不一定要json形式,這里說的是一種常見的做法)
4、用戶得到 token,存在 localStorage、cookie 或其它數據存儲形式中。以后用戶請求 /protected 中的 API 時,在請求的 header 中加入 Authorization: Bearer xxxx(token)。此處注意token之前有一個7字符長度的 Bearer。
5、服務器端對此 token 進行檢驗,如果合法就解析其中內容,根據其擁有的權限和自己的業務邏輯給出對應的響應結果。
6、用戶取得結果
如下如所示:

來看一下 JWT:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
token 分成了三部分,頭部(header),荷載(Payload) 和 簽名(Signature),每部分用 . 分隔,其中頭部和荷載使用了base64編碼,分別解碼之后得到兩個JSON串:
第一部分-頭部:
{
"alg": "HS256",
"typ": "JWT"
}
alg字段為加密算法,這是告訴我們 HMAC 采用 HS512 算法對 JWT 進行的簽名。
第二部分-荷載:
{
"sub": "1234567890",
"name": "John Doe",
"iat": 1516239022
}
荷載的字段及含義:
- iss: 該JWT的簽發者
- sub: 該JWT所面向的用戶
- aud: 接收該JWT的一方
- exp(expires): 什么時候過期,這里是一個Unix時間戳
- iat(issued at): 在什么時候簽發的
這段告訴我們這個Token中含有的數據聲明(Claim),這個例子里面有三個聲明:sub, name 和 iat。在我們這個例子中,分別代表着
所面向的用戶、用戶名、創建時間,當然你可以把任意數據聲明在這里。
第三部分-簽名:
第三部分簽名則不能使用base64解碼出來,該部分用於驗證頭部和荷載數據的完整性。
JWT的生成和解析
引入依賴:
<!-- JWT -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
創建一個測試類嘗試一下 JWT 的生成:
public class Test {
public static void main(String[] args){
String token = Jwts.builder()
主題 放入用戶名
.setSubject("niceyoo")
自定義屬性 放入用戶擁有請求權限
.claim("authorities","admin")
失效時間
.setExpiration(new Date(System.currentTimeMillis() + 7 * 60 * 1000))
簽名算法和密鑰
.signWith(SignatureAlgorithm.HS512, "tmax")
.compact();
System.out.println(token);
}
}
控制台打印如下:
eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJuaWNleW9vIiwiYXV0aG9yaXRpZXMiOiJhZG1pbiIsImV4cCI6MTU1OTQ1ODM1M30.keCiHrcEr0IWXfZLocgHS8znn7uSiaZW1IT6bTs-EQG0NPsb6-Aw_XbGQea4mez2CcAflgMqtzIpsDjZsUOVug
數據聲明(Claim)是一個自定義屬性,可以用來放入用戶擁有請求權限。上邊為簡單直接傳了一個 'admin'。
再看看解析:
public static void main(String[] args){
try {
解析token
Claims claims = Jwts.parser()
.setSigningKey("tmax")
.parseClaimsJws("eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJuaWNleW9vIiwiYXV0aG9yaXRpZXMiOiJhZG1pbiIsImV4cCI6MTU1OTQ1OTc2Mn0.MkSJtGaVePLa-eM3gylh1T3fwODg-6ceDDOxscXAQKun-qNrbQFcKPNqXhblbXPNLhaJyEnwugNANCTs98UNmA")
.getBody();
System.out.println(claims);
獲取用戶名
String username = claims.getSubject();
System.out.println("username:"+username);
獲取權限
String authority = claims.get("authorities").toString();
System.out.println("權限:"+authority);
} catch (ExpiredJwtException e) {
System.out.println("jwt異常");
} catch (Exception e){
System.out.println("異常");
}
}
控制台打印:
{sub=niceyoo, authorities=admin, exp=1559459762}
username:niceyoo
權限:admin
JWT 本身沒啥難度,但安全整體是一個比較復雜的事情,JWT 只不過提供了一種基於 token 的請求驗證機制。但我們的用戶權限,對於 API 的權限划分、資源的權限划分,用戶的驗證等等都不是JWT負責的。也就是說,請求驗證后,你是否有權限看對應的內容是由你的用戶角色決定的。所接下來才是我們的重點,Spring Security 整合 JWT。
集成JWT
要想要 JW T在 Spring 中工作,我們應該新建一個 JWT filter,並把它配置在 WebSecurityConfig 中。
WebSecurityConfigurerAdapter.java
@Slf4j
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled=true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserDetailsServiceImpl userDetailsService;
@Autowired
private AuthenticationSuccessHandler successHandler;
@Autowired
private AuthenticationFailHandler failHandler;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService).passwordEncoder(new BCryptPasswordEncoder());加密
}
@Override
protected void configure(HttpSecurity http) throws Exception {
ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry registry = http
.authorizeRequests();
registry.and()
表單登錄方式
.formLogin()
.permitAll()
成功處理類
.successHandler(successHandler)
失敗
.failureHandler(failHandler)
.and()
.logout()
.permitAll()
.and()
.authorizeRequests()
任何請求
.anyRequest()
需要身份認證
.authenticated()
.and()
關閉跨站請求防護
.csrf().disable()
前后端分離采用JWT 不需要session
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
添加JWT過濾器 除已配置的其它請求都需經過此過濾器
.addFilter(new JWTAuthenticationFilter(authenticationManager(), 7));
}
}
相較於上一篇主要多了如下一行配置:
.addFilter(new JWTAuthenticationFilter(authenticationManager(), 7));
JWTAuthenticationFilter.java
@Slf4j
public class JWTAuthenticationFilter extends BasicAuthenticationFilter {
private Integer tokenExpireTime;
public JWTAuthenticationFilter(AuthenticationManager authenticationManager, Integer tokenExpireTime) {
super(authenticationManager);
this.tokenExpireTime = tokenExpireTime;
}
public JWTAuthenticationFilter(AuthenticationManager authenticationManager, AuthenticationEntryPoint authenticationEntryPoint) {
super(authenticationManager, authenticationEntryPoint);
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
String header = request.getHeader(SecurityConstant.HEADER);
if(StrUtil.isBlank(header)){
header = request.getParameter(SecurityConstant.HEADER);
}
Boolean notValid = StrUtil.isBlank(header) || (!header.startsWith(SecurityConstant.TOKEN_SPLIT));
if (notValid) {
chain.doFilter(request, response);
return;
}
try {
UsernamePasswordAuthenticationToken 繼承 AbstractAuthenticationToken 實現 Authentication
所以當在頁面中輸入用戶名和密碼之后首先會進入到 UsernamePasswordAuthenticationToken驗證(Authentication),
UsernamePasswordAuthenticationToken authentication = getAuthentication(header, response);
SecurityContextHolder.getContext().setAuthentication(authentication);
}catch (Exception e){
e.toString();
}
chain.doFilter(request, response);
}
private UsernamePasswordAuthenticationToken getAuthentication(String header, HttpServletResponse response) {
用戶名
String username = null;
權限
List<GrantedAuthority> authorities = new ArrayList<>();
try {
解析token
Claims claims = Jwts.parser()
.setSigningKey(SecurityConstant.JWT_SIGN_KEY)
.parseClaimsJws(header.replace(SecurityConstant.TOKEN_SPLIT, ""))
.getBody();
logger.info("claims:"+claims);
獲取用戶名
username = claims.getSubject();
logger.info("username:"+username);
獲取權限
String authority = claims.get(SecurityConstant.AUTHORITIES).toString();
logger.info("authority:"+authority);
if(!StringUtils.isEmpty(authority)){
authorities.add(new SimpleGrantedAuthority(authority));
}
} catch (ExpiredJwtException e) {
ResponseUtil.out(response, ResponseUtil.resultMap(false,401,"登錄已失效,請重新登錄"));
} catch (Exception e){
log.error(e.toString());
ResponseUtil.out(response, ResponseUtil.resultMap(false,500,"解析token錯誤"));
}
if(StrUtil.isNotBlank(username)) {
踩坑提醒 此處password不能為null
User principal = new User(username, "", authorities);
return new UsernamePasswordAuthenticationToken(principal, null, authorities);
}
return null;
}
}
接下來我們啟動項目看看:
訪問項目中已有的鏈接:
http://localhost:7777/tmax/videoCategory/getAll
老樣子認證一波:

其中 niceyoo、 為數據庫用戶信息
登陸成功后獲取返回的 token,注意,此 token 是由 JWT 生成的:
String token = SecurityConstant.TOKEN_SPLIT + Jwts.builder()
主題 放入用戶名
.setSubject(username)
自定義屬性 放入用戶擁有請求權限
.claim(SecurityConstant.AUTHORITIES, authorities)
失效時間
.setExpiration(new Date(System.currentTimeMillis() + 7 * 60 * 1000))
簽名算法和密鑰
.signWith(SignatureAlgorithm.HS512, SecurityConstant.JWT_SIGN_KEY)
.compact();
瀏覽器返回 token 如下:

然后我們通過 token 憑證去訪問上邊的方法:

后台打印信息:
claims:{sub=niceyoo, authorities=admin, exp=1559472866}
username:niceyoo
authority:admin
隨便改一下 token ,返回如下:

