SpringSecurity詳解整合JWT實現(xiàn)全過程
Token
Token和Sessionid的思想一樣。Session是存在服務(wù)器端JVM中,Token存在Redis中。
解決分布式Session數(shù)據(jù)一致性問題:Spring-Session
傳統(tǒng)的Token,例如:用戶登錄成功生成對應(yīng)的令牌,key:為令牌, value:userid,隱藏了數(shù)據(jù)真實性 ,同時將該token存放到redis中,返回對應(yīng)的真實令牌給客戶端存放??蛻舳嗣看卧L問后端請求的時候,會傳遞該token在請求中,服務(wù)器端接收到該token之后,從redis中查詢?nèi)绻嬖诘那闆r下,則說明在有效期內(nèi),如果在Redis中不存在的情況下,則說明過期或者token錯誤。
Token使用:
- 驗證賬號密碼成功
- 生成一個令牌UUID
- 將該令牌存放到redis中,key為令牌,value值對應(yīng)存放userid
- 最終返回令牌給客戶端
Token驗證回話信息
- 在請求頭中傳遞該令牌
- 從Redis中驗證該令牌是否有效期
- 獲取value內(nèi)容
- 根據(jù)userid查詢用戶信息,返回給客戶端
Token存放數(shù)據(jù)優(yōu)缺點:
缺點:
- 必須依賴服務(wù)器,占用服務(wù)器端資源
- 效率非常低
優(yōu)點:
- 可以隱藏數(shù)據(jù)真實性
- 適用于分布式/微服務(wù)
- 安全性高 JWT
Jwt
JSON WEB Token JWT的聲明一般被用來在身份提供者和服務(wù)提供者間傳遞被認(rèn)證的用戶身份信息,以便于從資源服務(wù)器獲取資源,也可以增加一些額外的其它業(yè)務(wù)邏輯所必須的聲明信息,該token也可直接被用于認(rèn)證,也可被加密。
組成
第一部分:header (頭部)
描述加密算法
HS256:屬于驗證簽名
RSA256:屬于非對稱加密
第二部分:playload(載荷)
攜帶存放的數(shù)據(jù) 用戶名稱、用戶頭像之類,需要注意銘感數(shù)據(jù)
標(biāo)準(zhǔn)中注冊的聲明 (建議但不強制使用) :
- iss: jwt簽發(fā)者
- sub: jwt所面向的用戶
- aud: 接收jwt的一方
- exp: jwt的過期時間,這個過期時間必須要大于簽發(fā)時間
- nbf: 定義在什么時間之前,該jwt都是不可用的.
- iat: jwt的簽發(fā)時間
- jti: jwt的唯一身份標(biāo)識,主要用來作為一次性token,從而回避重放攻擊。
第三部分:secret (存放在服務(wù)器端)
簽名值,Base64(header .playload) +秘鑰
JWT和Token的區(qū)別
1、token對應(yīng)的數(shù)據(jù)存放在redis中
2、JWT對應(yīng)存放的數(shù)據(jù)(payload中)客戶端
優(yōu)缺點
優(yōu)點:
1、JWT數(shù)據(jù)存放在客戶端,不依賴于服務(wù)器端,減輕服務(wù)器端壓力
2、效率比傳統(tǒng)的token驗證還要高
缺點
1、jwt一旦生成之后后期無法修改
2、無法銷毀一個jwt
3、建議不要放敏感數(shù)據(jù),userid、手機(jī)號
4、后端無法統(tǒng)計 生成JWT
手寫JWT
public class Test001 { private static final String SIGN_KEY = "kaicoSignKey"; public static void main(String[] args) throws UnsupportedEncodingException { //手寫jwt 封裝三個部分:header、payload、sign簽名 //定義header JSONObject header = new JSONObject(); header.put("alg", "HS256"); //payload JSONObject payload = new JSONObject(); payload.put("name", "kaico"); String headerEncode = Base64.getEncoder().encodeToString(header.toJSONString().getBytes()); String payloadJSONString = payload.toJSONString(); String payloadEncode = Base64.getEncoder().encodeToString(payloadJSONString.getBytes()); //sign簽名值 實際上就是 md5 String sign = DigestUtils.md5DigestAsHex((payload + SIGN_KEY).getBytes()); String jwt = headerEncode + "." + payloadEncode + "." + sign; System.out.println(jwt); //解密 String payloadEncodeStr = jwt.split("\\.")[1]; String payloadDecoder = new String(Base64.getDecoder().decode(payloadEncodeStr), "UTF-8"); String newSign = DigestUtils.md5DigestAsHex((payloadDecoder + SIGN_KEY).getBytes()); System.out.println(newSign.equals(jwt.split("\\.")[2])); } }
使用工具類新建JWT
public class Test003 { private static final String SIGN_KEY = "kaicoSignKey"; public static void main(String[] args) { long now = System.currentTimeMillis(); //設(shè)置過期時間 (測試使用1秒鐘) Long exp = now + 1 * 1000; JwtBuilder jwtBuilder = Jwts.builder() //payload值 .claim("userImg", "sssss") //簽名值 .signWith(SignatureAlgorithm.HS256, SIGN_KEY) .setExpiration(new Date(exp)); //輸出JWT的內(nèi)容 String jwt = jwtBuilder.compact(); System.out.println(jwt); //解密 try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } Claims body = Jwts.parser().setSigningKey(SIGN_KEY).parseClaimsJws(jwt).getBody(); System.out.println(body.get("userImg")); } }
Springboot整合JWT
登錄流程
1、驗證賬號密碼
2、賬號密碼驗證成功,生成JWT返回給客戶端(移動app、瀏覽器、微信小程序)
3、客戶端請求服務(wù)端,服務(wù)端驗證JWT
1. base64解密jwt獲取payload中的數(shù)據(jù)
2. 獲取roles權(quán)限列表注冊到SpringSecurity框架中
代碼整合
在上次整合SpringSecurity的基礎(chǔ)上
新增兩個過濾器
package com.kaico.jwt.filter; import com.fasterxml.jackson.databind.ObjectMapper; import com.kaico.jwt.entity.UserEntity; import com.kaico.jwt.utils.JwtUtils; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import javax.servlet.FilterChain; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.util.ArrayList; /** * @Author kaico * @Description //TODO * @Date 19:26 2022/7/25 */ public class JWTLoginFilter extends UsernamePasswordAuthenticationFilter { /** * 獲取授權(quán)管理 */ private AuthenticationManager authenticationManager; public JWTLoginFilter(AuthenticationManager authenticationManager) { this.authenticationManager = authenticationManager; /** * 后端登陸接口 */ super.setFilterProcessesUrl("/auth/login"); } @Override public Authentication attemptAuthentication(HttpServletRequest req, HttpServletResponse res) { try { UserEntity user = new ObjectMapper() .readValue(req.getInputStream(), UserEntity.class); return authenticationManager.authenticate( new UsernamePasswordAuthenticationToken( user.getUsername(), user.getPassword(), new ArrayList<>()) ); } catch (IOException e) { logger.error(e.getMessage()); return null; } } @Override /** * 用戶登陸成功之后驗證 */ protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException { UserEntity userEntity = (UserEntity) authResult.getPrincipal(); String jwtToken = JwtUtils.generateJsonWebToken(userEntity); response.addHeader("token", jwtToken); } /** * 賬號或者密碼錯誤 * @param request * @param response * @param failed * @throws IOException * @throws ServletException */ @Override protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException { response.setCharacterEncoding("UTF-8"); response.getWriter().print("賬號或者密碼錯誤"); } }
package com.kaico.jwt.filter; import com.kaico.jwt.utils.JwtUtils; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.web.authentication.www.BasicAuthenticationFilter; import javax.servlet.FilterChain; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.util.List; /** * @Author kaico * @Description //TODO * @Date 19:37 2022/7/25 */ public class JWTValidationFilter extends BasicAuthenticationFilter { public JWTValidationFilter(AuthenticationManager authenticationManager) { super(authenticationManager); } /** * 過濾請求驗證 * * @param request * @param response * @param chain * @throws IOException * @throws ServletException */ @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException { SecurityContextHolder.getContext().setAuthentication(setAuthentication(request.getHeader("token"))); super.doFilterInternal(request, response, chain); } /** * 驗證token 并且驗證權(quán)限 * @param token * @return */ private UsernamePasswordAuthenticationToken setAuthentication(String token) { String username = JwtUtils.getUsername(token); if (username == null) { return null; } //解析權(quán)限列表 List<SimpleGrantedAuthority> userRoleList = JwtUtils.getUserRole(token); return new UsernamePasswordAuthenticationToken(username, null, userRoleList); } }
JWT工具類
package com.kaico.jwt.utils; import com.alibaba.fastjson.JSONArray; import com.kaico.jwt.entity.UserEntity; import io.jsonwebtoken.Claims; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.SignatureAlgorithm; import org.springframework.security.core.authority.SimpleGrantedAuthority; import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Map; public class JwtUtils { public static final String TOKEN_HEADER = "token"; public static final String TOKEN_PREFIX = "Bearer "; private static final String SUBJECT = "kaico"; //JWT有效期 private static final long EXPIRITION = 1000 * 24 * 60 * 60 * 7; private static final String APPSECRET_KEY = "kaico_secret"; private static final String ROLE_CLAIMS = "roles"; public static String generateJsonWebToken(UserEntity user) { String token = Jwts .builder() .setSubject(SUBJECT) .claim(ROLE_CLAIMS, user.getAuthorities()) .claim("username", user.getUsername()) .setIssuedAt(new Date()) .setExpiration(new Date(System.currentTimeMillis() + EXPIRITION)) .signWith(SignatureAlgorithm.HS256, APPSECRET_KEY).compact(); return token; } /** * 生成token * * @param username * @param role * @return */ public static String createToken(String username, String role) { Map<String, Object> map = new HashMap<>(); map.put(ROLE_CLAIMS, role); String token = Jwts .builder() .setSubject(username) .setClaims(map) .claim("username", username) .setIssuedAt(new Date()) .setExpiration(new Date(System.currentTimeMillis() + EXPIRITION)) .signWith(SignatureAlgorithm.HS256, APPSECRET_KEY).compact(); return token; } public static Claims checkJWT(String token) { try { final Claims claims = Jwts.parser().setSigningKey(APPSECRET_KEY).parseClaimsJws(token).getBody(); return claims; } catch (Exception e) { e.printStackTrace(); return null; } } /** * 獲取用戶名 * * @param token * @return */ public static String getUsername(String token) { Claims claims = Jwts.parser().setSigningKey(APPSECRET_KEY).parseClaimsJws(token).getBody(); return claims.get("username").toString(); } /** * 獲取用戶角色 * * @param token * @return */ public static List<SimpleGrantedAuthority> getUserRole(String token) { Claims claims = Jwts.parser().setSigningKey(APPSECRET_KEY).parseClaimsJws(token).getBody(); List roles = (List) claims.get(ROLE_CLAIMS); String json = JSONArray.toJSONString(roles); List<SimpleGrantedAuthority> grantedAuthorityList = JSONArray.parseArray(json, SimpleGrantedAuthority.class); return grantedAuthorityList; } /** * 是否過期 * * @param token * @return */ public static boolean isExpiration(String token) { Claims claims = Jwts.parser().setSigningKey(APPSECRET_KEY).parseClaimsJws(token).getBody(); return claims.getExpiration().before(new Date()); } }
修改SecurityConfig
配置類
@Override protected void configure(HttpSecurity http) throws Exception { List<PermissionEntity> allPermission = permissionMapper.findAllPermission(); ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry expressionInterceptUrlRegistry = http.authorizeRequests(); allPermission.forEach((permission) -> { expressionInterceptUrlRegistry.antMatchers(permission.getUrl()). hasAnyAuthority(permission.getPermTag()); }); // 配置前后令牌登陸 expressionInterceptUrlRegistry.antMatchers("/auth/login").permitAll() .antMatchers("/**").fullyAuthenticated() .and() //配置過濾器 .addFilter(new JWTValidationFilter(authenticationManager())) .addFilter(new JWTLoginFilter(authenticationManager())).csrf().disable() //提出session .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS); }
測試:
1、post方式請求登錄接口:localhost:8080/auth/login
請求參數(shù)json,返回請求頭中帶有token
{
"username":"kaico_add",
"password":"kaico"
}
2、再次請求其他接口時,請求頭上帶上token,
存在的問題
Jwt如何實現(xiàn)注銷?
- 客戶端清除緩存,比如:瀏覽器cookie清除(但是服務(wù)器還是存在)
- 權(quán)限發(fā)生變化的情況下,管理員同志用戶重新登錄或者提示權(quán)限不足,請聯(lián)系管理員開放權(quán)限。
- 建議將時間設(shè)置稍微短一點
- 使用黑名單過濾,后期對服務(wù)器端壓力大
JWT是否安全?
安全機(jī)制肯定有
就算黑客篡改payload中的權(quán)限列表,必須先獲取到服務(wù)器端秘鑰,自己生產(chǎn)JWT才行。
JWT中存放userid
可以單獨對userid做對稱加密之后再存在payload中,解密的秘鑰在服務(wù)器端,也是安全的。
以上就是SpringSecurity詳解整合JWT實現(xiàn)全過程的詳細(xì)內(nèi)容,更多關(guān)于SpringSecurity JWT的資料請關(guān)注腳本之家其它相關(guān)文章!
- SpringSecurity+Redis+Jwt實現(xiàn)用戶認(rèn)證授權(quán)
- springboot+springsecurity+mybatis+JWT+Redis?實現(xiàn)前后端離實戰(zhàn)教程
- SpringBoot3.0+SpringSecurity6.0+JWT的實現(xiàn)
- SpringSecurity整合JWT的使用示例
- SpringBoot整合SpringSecurity和JWT和Redis實現(xiàn)統(tǒng)一鑒權(quán)認(rèn)證
- SpringBoot+SpringSecurity+jwt實現(xiàn)驗證
- mall整合SpringSecurity及JWT認(rèn)證授權(quán)實戰(zhàn)下
- mall整合SpringSecurity及JWT實現(xiàn)認(rèn)證授權(quán)實戰(zhàn)
- Java SpringSecurity+JWT實現(xiàn)登錄認(rèn)證
- springSecurity+jwt使用小結(jié)
相關(guān)文章
SpringBoot +Vue開發(fā)考試系統(tǒng)的教程
這篇文章主要介紹了SpringBoot +Vue開發(fā)考試系統(tǒng),支持多種題型:選擇題、多選題、判斷題、填空題、綜合題以及數(shù)學(xué)公式。支持在線考試,教師在線批改試卷。本文通過實例代碼給大家介紹的非常詳細(xì),需要的朋友可以參考下2020-05-05一文深入理解Java中的java.lang.reflect.InvocationTargetException錯誤
這篇文章主要給大家介紹了關(guān)于Java中java.lang.reflect.InvocationTargetException錯誤的相關(guān)資料,java.lang.reflect.InvocationTargetException是Java中的一個異常類,它通常是由反射調(diào)用方法時拋出的異常,需要的朋友可以參考下2024-03-03idea快速找到項目中對應(yīng)的類圖文詳解(包括源碼)
用IDEA開發(fā)Java項目時經(jīng)常會使用到各種快捷鍵,其中搜索是最常用的之一,下面這篇文章主要給大家介紹了關(guān)于idea如何快速找到項目中對應(yīng)的類(包括源碼)的相關(guān)資料,文中通過圖文介紹的非常詳細(xì),需要的朋友可以參考下2023-06-06mybatis-plus如何修改日志只打印SQL語句不打印查詢結(jié)果
這篇文章主要介紹了mybatis-plus如何修改日志只打印SQL語句不打印查詢結(jié)果問題,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教2024-06-06springboot?@Validated的概念及示例實戰(zhàn)
這篇文章主要介紹了springboot?@Validated的概念以及實戰(zhàn),使用?@Validated?注解,Spring?Boot?應(yīng)用可以有效地實現(xiàn)輸入驗證,提高數(shù)據(jù)的準(zhǔn)確性和應(yīng)用的安全性,本文結(jié)合實例給大家講解的非常詳細(xì),需要的朋友可以參考下2024-04-04