javascript
SpringSecurity 整合 JWT
項目集成Spring Security(一)
在上一篇基礎上繼續(xù)集成 JWT ,實現(xiàn)用戶身份驗證。
前言
前后端分離項目中,如果直接把 API 接口對外開放,我們知道這樣風險是很大的,所以在上一篇中我們引入了 Spring Security ,但是我們在登陸后缺少了請求憑證部分。
什么是JWT?
JWT是 Json Web Token 的縮寫。它是基于 RFC 7519 標準定義的一種可以安全傳輸?shù)?小巧 和 自包含 的JSON對象。由于數(shù)據(jù)是使用數(shù)字簽名的,所以是可信任的和安全的。JWT可以使用HMAC算法對secret進行加密或者使用RSA的公鑰私鑰對來進行簽名。
JWT的工作流程
1、用戶進入登錄頁,輸入用戶名、密碼,進行登錄;
2、服務器驗證登錄鑒權,如果改用戶合法,根據(jù)用戶的信息和服務器的規(guī)則生成 JWT Token
3、服務器將該 token 以 json 形式返回(不一定要json形式,這里說的是一種常見的做法)
4、用戶得到 token,存在 localStorage、cookie 或其它數(shù)據(jù)存儲形式中。以后用戶請求 /protected 中的 API 時,在請求的 header 中加入 Authorization: Bearer xxxx(token)。此處注意token之前有一個7字符長度的 Bearer。
5、服務器端對此 token 進行檢驗,如果合法就解析其中內(nèi)容,根據(jù)其擁有的權限和自己的業(yè)務邏輯給出對應的響應結(jié)果。
6、用戶取得結(jié)果
如下如所示:
來看一下 JWT:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5ctoken 分成了三部分,頭部(header),荷載(Payload) 和 簽名(Signature),每部分用 . 分隔,其中頭部和荷載使用了base64編碼,分別解碼之后得到兩個JSON串:
第一部分-頭部:
{"alg": "HS256","typ": "JWT" }alg字段為加密算法,這是告訴我們 HMAC 采用 HS512 算法對 JWT 進行的簽名。
第二部分-荷載:
{"sub": "1234567890","name": "John Doe","iat": 1516239022 }荷載的字段及含義:
- iss: 該JWT的簽發(fā)者
- sub: 該JWT所面向的用戶
- aud: 接收該JWT的一方
- exp(expires): 什么時候過期,這里是一個Unix時間戳
- iat(issued at): 在什么時候簽發(fā)的
這段告訴我們這個Token中含有的數(shù)據(jù)聲明(Claim),這個例子里面有三個聲明:sub, name 和 iat。在我們這個例子中,分別代表著
所面向的用戶、用戶名、創(chuàng)建時間,當然你可以把任意數(shù)據(jù)聲明在這里。
第三部分-簽名:
第三部分簽名則不能使用base64解碼出來,該部分用于驗證頭部和荷載數(shù)據(jù)的完整性。
JWT的生成和解析
引入依賴:
<!-- JWT --> <dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt</artifactId><version>0.9.1</version> </dependency>創(chuàng)建一個測試類嘗試一下 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數(shù)據(jù)聲明(Claim)是一個自定義屬性,可以用來放入用戶擁有請求權限。上邊為簡單直接傳了一個 ‘a(chǎn)dmin’。
再看看解析:
public static void main(String[] args){try {解析tokenClaims 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 權限:adminJWT 本身沒啥難度,但安全整體是一個比較復雜的事情,JWT 只不過提供了一種基于 token 的請求驗證機制。但我們的用戶權限,對于 API 的權限劃分、資源的權限劃分,用戶的驗證等等都不是JWT負責的。也就是說,請求驗證后,你是否有權限看對應的內(nèi)容是由你的用戶角色決定的。所接下來才是我們的重點,Spring Security 整合 JWT。
集成JWT
要想要 JW T在 Spring 中工作,我們應該新建一個 JWT filter,并把它配置在 WebSecurityConfig 中。
WebSecurityConfigurerAdapter.java
@Slf4j @Configuration @EnableGlobalMethodSecurity(prePostEnabled=true) public class WebSecurityConfig extends WebSecurityConfigurerAdapter {@Autowiredprivate UserDetailsServiceImpl userDetailsService;@Autowiredprivate AuthenticationSuccessHandler successHandler;@Autowiredprivate AuthenticationFailHandler failHandler;@Overrideprotected void configure(AuthenticationManagerBuilder auth) throws Exception {auth.userDetailsService(userDetailsService).passwordEncoder(new BCryptPasswordEncoder());加密}@Overrideprotected 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過濾器 除已配置的其它請求都需經(jīng)過此過濾器.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);}@Overrideprotected 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 實現(xiàn) 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 {解析tokenClaims 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不能為nullUser principal = new User(username, "", authorities);return new UsernamePasswordAuthenticationToken(principal, null, authorities);}return null;} }接下來我們啟動項目看看:
訪問項目中已有的鏈接:
http://localhost:7777/tmax/videoCategory/getAll老樣子認證一波:
其中 niceyoo、****** 為數(shù)據(jù)庫用戶信息
登陸成功后獲取返回的 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 ,返回如下:
總結(jié)
以上是生活随笔為你收集整理的SpringSecurity 整合 JWT的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: C++的ORM工具比较
- 下一篇: 使用UGUI绘制自定义几何图形