|
作者:賜我白日夢(mèng) cnblogs.com/ZhuChangwu/p/11345098.html
無(wú)狀態(tài)登錄原理先提一下啥是有狀態(tài)登錄單臺(tái)tomcat的情況下,編碼的流程如下: 前端提交表單里用戶的輸入的賬號(hào)密碼 后臺(tái)接受,查數(shù)據(jù)庫(kù), 在數(shù)據(jù)庫(kù)中找到用戶的信息后,把用戶的信息存放到session里面,返回給用戶cookie 以后用戶的請(qǐng)求都會(huì)自動(dòng)攜帶著cookie去訪問(wèn)后臺(tái),后臺(tái)根據(jù)用戶的cookie辨識(shí)用戶的身份
但是有缺點(diǎn) 如果有千千萬(wàn)的用戶都往session里面存放信息, session很難跨服務(wù)器,也就是說(shuō),用戶每次請(qǐng)求,都必須訪問(wèn)同一臺(tái)tomcat,新的tomcat不認(rèn)識(shí)用戶的cookie
無(wú)狀態(tài)登錄對(duì)于服務(wù)端,不再保存任何用戶的信息,對(duì)于他們來(lái)說(shuō),所有的用戶地位平等 對(duì)于用戶,他們需要自己攜帶著信息去訪問(wèn)服務(wù)端,攜帶的信息可以被所有服務(wù)端辨認(rèn),所以,對(duì)于用戶,所有的服務(wù)地位平等
具體如何實(shí)現(xiàn)呢?用戶登錄,服務(wù)端返回給用戶的個(gè)人信息+token令牌 前端為了自己使用方便,將用戶的個(gè)人信息緩存進(jìn)瀏覽器(因?yàn)楹蠖酥皇墙o了他一個(gè)token) 用戶攜帶自己的token去訪問(wèn)服務(wù)端 認(rèn)證通過(guò),把用戶請(qǐng)求的數(shù)據(jù)返回給客戶端 以后不論用戶請(qǐng)求哪個(gè)微服務(wù),都攜帶著token 微服務(wù)對(duì)token進(jìn)行解密,判斷他是否有效

JWT(Json Web Token)生成規(guī)則整個(gè)登錄.授權(quán).鑒權(quán)的過(guò)程token的安全性至關(guān)重要,而JWT就是一門有關(guān)于如何生成一個(gè)不可仿造的token的規(guī)范 他是JSON風(fēng)格輕量級(jí)的授權(quán)和身份認(rèn)證規(guī)范,可實(shí)現(xiàn)無(wú)狀態(tài)、分布式的Web應(yīng)用授權(quán),而且它不是技術(shù),和語(yǔ)言無(wú)關(guān),java有對(duì)這個(gè)規(guī)范的實(shí)現(xiàn) 叫做 jjwt。 jjwt的github項(xiàng)目 https://github.com/jwtk/jjwt

由JWT算法得到的token格式如上圖,由頭部,載荷,簽名三部分組成 頭部 由兩部分組成,alg表示使用的加密算法 typ表示使用的token的類型 載荷 存放用戶相關(guān)的信息 // 下面是它已經(jīng)定義好的載荷部分key,也允許我們自定義載荷部分key iss: jwt簽發(fā)者 sub: jwt所面向的用戶 aud: 接收jwt的一方 exp: jwt的過(guò)期時(shí)間,這個(gè)過(guò)期時(shí)間必須要大于簽發(fā)時(shí)間 nbf: 定義在什么時(shí)間之前,該jwt都是不可用的. iat: jwt的簽發(fā)時(shí)間 jti: jwt的唯一身份標(biāo)識(shí),主要用來(lái)作為一次性token,從而回避重放攻擊。
第三步分的簽名是由 頭+載荷+鹽 三部分加密組成 從圖可以看出,頭部和載荷被串改后,生成的編碼會(huì)發(fā)生改變,因此保證了token的安全 ,還有,載荷部分可解密,因此不要往里面放入敏感的信息(比如密碼 ) 只要密鑰不泄露,別人就無(wú)法偽造任何信息 jwt的交互過(guò)程 
RSA非對(duì)稱加密算算法由美國(guó)麻 省理工 學(xué)院三 位學(xué)者 Riv est、Sh amir 及Adleman 研 究發(fā) 展出 一套 可實(shí) 際使用 的公 開(kāi)金 鑰密 碼系 統(tǒng),那就是 RSA(Rivest-Shamir-Adleman)密碼系統(tǒng) jwt(是一種非對(duì)稱加密算法) JWT不一定非要搭配RSA算法,但是擁有RSA算法公鑰私鑰的特性,會(huì)使我們的業(yè)務(wù)邏輯變的簡(jiǎn)單,做到校驗(yàn)變少 對(duì)稱加密,如AES(Advanced Encryption Standard) 高級(jí)加密標(biāo)準(zhǔn) 非對(duì)稱加密,如RSA 不可逆加密,如MD5,SHA 使用JJWT實(shí)現(xiàn)JWTJJWT(java json web token) <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.9.0</version> </dependency>
創(chuàng)建jwt令牌 token最終會(huì)頒發(fā)給前端,這時(shí)候就得去和前端的哥們商量它想要什么信息 // 生成令牌,主要是用它生成載荷 JwtBuilder builder = Jwts.builder() // 設(shè)置頭部,使用hs256加密, + key,也就是鹽 .signWith(SignatureAlgorithm.HS256,'changwu') // 添加載荷 .setId('666') // 用戶id .setSubject('張三') // 用戶名 .setExpiration(new Date(new Date().getTime()+60*1000)) // 過(guò)期時(shí)間 .setIssuedAt(new Date())// 登錄時(shí)間 // 添加自定義的鍵值對(duì) .claim('role','admin'); System.out.println(builder.compact());
經(jīng)過(guò)它處理的token長(zhǎng)這個(gè)樣子, 三部組成 XXX.YYY.ZZZ
解析token 能成功解析出結(jié)果的前提是兩次的鹽是一樣的才行 Claims map = Jwts.parser().setSigningKey('changwu') .parseClaimsJws('eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiI2NjYiLCJzdWIiOiLlvKDkuIkiLCJleHAiOjE1NjU2MTg1MjUsImlhdCI6MTU2NTYxODQ2NSwicm9sZSI6ImFkbWluIn0.GDVfLq-ehSnMCRoxVcziXkirjOg34SUUPBK5vAEHu80') .getBody();
System.out.println('用戶id' + map.getId()); System.out.println('用戶名' + map.getSubject()); System.out.println('token過(guò)期時(shí)間' + new SimpleDateFormat('yyyy-MM-dd HH:mm:ss').format(map.getExpiration())); System.out.println('用戶登錄時(shí)間' + new SimpleDateFormat('yyyy-MM-dd HH:mm:ss').format(map.getIssuedAt())); System.out.println('用戶的角色是:'+map.get('role'));
攔截器注意哦,使用的是SpringMvc的攔截器,而不是Servlet的過(guò)濾器。擴(kuò)展:SpringBoot實(shí)現(xiàn)過(guò)濾器、攔截器與切片 攔截器的體系架構(gòu)
在攔截器的體系中,我們常用的是上面的來(lái)兩個(gè) HandlerInterceptor是頂級(jí)接口如下: 
雖然是接口, 但是擁有jdk8的特性,是默認(rèn)的方法,所以允許我們挑選它的部分方法實(shí)現(xiàn)而不會(huì)報(bào)錯(cuò) prehandler: 請(qǐng)求到達(dá)控制器之間被回調(diào),可以在這里進(jìn)行設(shè)置編碼,安全控制,權(quán)限校驗(yàn), 一般全部返回ture,表示放行 postHandler: 控制器處理請(qǐng)求之后生成了ModelAndView,但是未進(jìn)行渲染,提供了修改ModelAndView的機(jī)會(huì) afterCompletion: 返回給用戶ModelAndView之后執(zhí)行, 用于收尾工作
第二個(gè)是HandlerInterceptorAdapter如下圖 
這個(gè)適配器方法全是空實(shí)現(xiàn),同樣可以滿足我們的需求,但是它同時(shí)實(shí)現(xiàn)了AsyncHandlerInterceptor,擁有了一個(gè)新的方法,afterConcrruentHandingStarted(request,response,handler) 這個(gè)方法會(huì)在Controller方法異步執(zhí)行時(shí)開(kāi)始執(zhí)行, 而Interceptor的postHandle方法則是需要等到Controller的異步執(zhí)行完才能執(zhí)行 編碼實(shí)現(xiàn)其實(shí)到這里改如何做已經(jīng)清晰明了 用戶登錄,授權(quán)授權(quán)的很簡(jiǎn)單 用戶發(fā)送登錄請(qǐng)求提交form表單 后端根據(jù)用戶名密碼查詢用戶的信息 把用戶的信息封裝進(jìn)jwt的載荷部分 返回給前端token
用戶再次請(qǐng)求,鑒權(quán)后臺(tái)會(huì)有很多方法需要指定權(quán)限的人才能訪問(wèn), 所謂鑒定權(quán)限,其實(shí)就是把前端放在請(qǐng)求頭中的token信息解析出來(lái),如果解析成功了,說(shuō)明用戶的合法的,否則就提示前端用戶沒(méi)有權(quán)限。擴(kuò)展閱讀:前后端分離模式下的權(quán)限設(shè)計(jì)方案 把token從請(qǐng)求頭中解析出來(lái)的過(guò)程,其實(shí)是在大量的重復(fù)性工作,所以我們放在攔截器中實(shí)現(xiàn) 使用攔截器兩步走第一步,繼承HandlerInterAdapter,選擇重寫它的方法 設(shè)計(jì)的邏輯,這個(gè)方法肯定要返回true, 因?yàn)楹笈_(tái)的方法中肯定存在大量的不需要任何權(quán)限就能訪問(wèn)的方法 所以這個(gè)方法的作用就是,解析出請(qǐng)求頭中的用戶的權(quán)限信息,重新放回到request中, 這樣每個(gè)需要進(jìn)行權(quán)限驗(yàn)證的請(qǐng)求,就不需要再進(jìn)行解析請(qǐng)求頭,而是直接使用當(dāng)前回調(diào)方法的處理結(jié)果
@Component public class RequestInterceptor extends HandlerInterceptorAdapter {
@Autowired JwtUtil jwtUtil;
// 在請(qǐng)求進(jìn)入處理器之前回調(diào)這個(gè)方法 @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 獲取請(qǐng)求頭 String header = request.getHeader('Authorization'); // 請(qǐng)求頭不為空進(jìn)行解析 if (StringUtils.isNotBlank(header)) { // 按照我們和前端約定的格式進(jìn)行處理 if (header.startsWith('Bearer ')){ // 得到令牌 String token = header.substring(7); // 驗(yàn)證令牌 try{ // 令牌的解析這里一定的try起來(lái),因?yàn)樗馕鲥e(cuò)誤的令牌時(shí),會(huì)報(bào)錯(cuò) // 當(dāng)然你也可以在自定義的jwtUtil中把異常 try起來(lái),這里就不用寫了 Claims claims = jwtUtil.parseJWT(token); String roles =(String) claims.get('roles'); System.err.println('roles=='+roles); if (roles!=null&&'admin'.equals(roles)){ request.setAttribute('role_admin',token); } if (roles!=null&&'user'.equals(roles)){ request.setAttribute('role_user',token); } }catch (Exception e){ throw new RuntimeException('令牌不存在'); } } } return true; } }
這樣 控制器中的方法需要進(jìn)行權(quán)限驗(yàn)證時(shí),就免去了解析的麻煩,直接從request中獲取即可 第二步,將攔截器注冊(cè)進(jìn)容器 @Configuration public class InterceptorConfig extends WebMvcConfigurationSupport {
@Autowired RequestInterceptor requestInterceptor;
// 注冊(cè)攔截器 protected void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(requestInterceptor) .addPathPatterns('/**') .excludePathPatterns('/user/login/**'); } }
END
|