javascript
VUE+SpringBoot+JWT实现token验证,SSO单点登录
?
Session的產(chǎn)生:
在說(shuō)session是啥之前,我們先來(lái)說(shuō)說(shuō)為什么會(huì)出現(xiàn)session會(huì)話,它出現(xiàn)的機(jī)理是什么?我們知道,我們用瀏覽器打開一個(gè)網(wǎng)頁(yè),用到的是HTTP協(xié)議,htpp協(xié)議是無(wú)狀態(tài)的,什么是無(wú)狀態(tài)呢?就是說(shuō)這一次請(qǐng)求和上一次請(qǐng)求是沒有任何關(guān)系的,互不認(rèn)識(shí)的,沒有關(guān)聯(lián)的,這種無(wú)狀態(tài)的好處是快速。
所以就會(huì)帶來(lái)一個(gè)問(wèn)題就是,我希望幾個(gè)請(qǐng)求的頁(yè)面要有關(guān)聯(lián),比如:我在www.a.com/login.html里面登陸了,我在www.a.com/index.html 也希望是登陸狀態(tài),但是,這是2個(gè)不同的頁(yè)面,也就是2個(gè)不同的HTTP請(qǐng)求,這2個(gè)HTTP請(qǐng)求是無(wú)狀態(tài)的,也就是無(wú)關(guān)聯(lián)的,所以無(wú)法單純的在index.html中讀取到它在login.html中已經(jīng)登陸了!
那咋搞呢?我不可能這2個(gè)頁(yè)面我都去登陸一遍吧。
(1)用笨方法這2個(gè)頁(yè)面都去查詢數(shù)據(jù)庫(kù),如果有登陸狀態(tài),就判斷是登陸的了。這種查詢數(shù)據(jù)庫(kù)的方案雖然可行,但是每次都要去查詢數(shù)據(jù)庫(kù)不是個(gè)事,會(huì)造成數(shù)據(jù)庫(kù)的壓力。
(2)所以正是這種訴求,這個(gè)時(shí)候,一個(gè)新的客戶端存儲(chǔ)數(shù)據(jù)方式出現(xiàn)了:cookie。cookie是把少量的信息存儲(chǔ)在用戶自己的電腦上,它在一個(gè)域名下是一個(gè)全局的,只要設(shè)置它的存儲(chǔ)路徑在域名www.a.com下 ,那么當(dāng)用戶用瀏覽器訪問(wèn)時(shí),前端js就可以從這個(gè)域名的任意頁(yè)面讀取cookie中的信息。所以就很好的解決了我在www.a.com/login.html頁(yè)面登陸了,我也可以在www.a.com/index.html獲取到這個(gè)登陸信息了。同時(shí)又不用反復(fù)去查詢數(shù)據(jù)庫(kù)。
雖然這種方案很不錯(cuò),也很快速方便,但是由于cookie 是存在用戶端,而且它本身存儲(chǔ)的尺寸大小也有限,最關(guān)鍵是用戶可以是可見的,并可以隨意的修改,很不安全。那如何又要安全,又可以方便的全局讀取信息呢?于是,這個(gè)時(shí)候,一種新的存儲(chǔ)會(huì)話機(jī)制:session 誕生了。
(3)從上面的描述來(lái)講,它就是在一次會(huì)話中解決2次HTTP的請(qǐng)求的關(guān)聯(lián),讓它們產(chǎn)生聯(lián)系,讓2兩個(gè)頁(yè)面都能讀取到找個(gè)這個(gè)全局的session信息。session信息存在于服務(wù)器端,所以也就很好的解決了安全問(wèn)題。
Springboot+Vue集成JWT實(shí)現(xiàn)token驗(yàn)證
(1)Json web token (JWT), 是為了在網(wǎng)絡(luò)應(yīng)用環(huán)境間傳遞聲明而執(zhí)行的一種基于JSON的開放標(biāo)準(zhǔn)((RFC 7519).定義了一種簡(jiǎn)潔的,自包含的方法用于通信雙方之間以JSON對(duì)象的形式安全的傳遞信息。因?yàn)閿?shù)字簽名的存在,這些信息是可信的,JWT可以使用HMAC算法或者是RSA的公私秘鑰對(duì)進(jìn)行簽名。
1. 用戶使用賬號(hào)和面發(fā)出post請(qǐng)求;
2. 服務(wù)器使用私鑰創(chuàng)建一個(gè)jwt;
3. 服務(wù)器返回這個(gè)jwt給瀏覽器;
4. 瀏覽器將該jwt串在請(qǐng)求頭中像服務(wù)器發(fā)送請(qǐng)求;
5. 服務(wù)器驗(yàn)證該jwt;
6. 返回響應(yīng)的資源給瀏覽器。
jwt的主要應(yīng)用場(chǎng)景
身份認(rèn)證在這種場(chǎng)景下,一旦用戶完成了登陸,在接下來(lái)的每個(gè)請(qǐng)求中包含JWT,可以用來(lái)驗(yàn)證用戶身份以及對(duì)路由,服務(wù)和資源的訪問(wèn)權(quán)限進(jìn)行驗(yàn)證。由于它的開銷非常小,可以輕松的在不同域名的系統(tǒng)中傳遞,所有目前在單點(diǎn)登錄(SSO)中比較廣泛的使用了該技術(shù)。 信息交換在通信的雙方之間使用JWT對(duì)數(shù)據(jù)進(jìn)行編碼是一種非常安全的方式,由于它的信息是經(jīng)過(guò)簽名的,可以確保發(fā)送者發(fā)送的信息是沒有經(jīng)過(guò)偽造的。
優(yōu)點(diǎn)
1.簡(jiǎn)潔(Compact): 可以通過(guò)URL,POST參數(shù)或者在HTTP header發(fā)送,因?yàn)閿?shù)據(jù)量小,傳輸速度也很快
2.自包含(Self-contained):負(fù)載中包含了所有用戶所需要的信息,避免了多次查詢數(shù)據(jù)庫(kù)
3.因?yàn)門oken是以JSON加密的形式保存在客戶端的,所以JWT是跨語(yǔ)言的,原則上任何web形式都支持。
4.不需要在服務(wù)端保存會(huì)話信息,特別適用于分布式微服務(wù)。
JWT的結(jié)構(gòu)
JWT是由三段信息構(gòu)成的,將這三段信息文本用.鏈接一起就構(gòu)成了JWT字符串。
就像這樣:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ
JWT包含了三部分:
Header 頭部(標(biāo)題包含了令牌的元數(shù)據(jù),并且包含簽名和/或加密算法的類型)
Payload 負(fù)載 (類似于飛機(jī)上承載的物品)
Signature 簽名/簽證
Header
JWT的頭部承載兩部分信息:token類型和采用的加密算法。
{ "alg": "HS256","typ": "JWT" }聲明類型:這里是jwt
聲明加密的算法:通常直接使用 HMAC SHA256
加密算法是單向函數(shù)散列算法,常見的有MD5、SHA、HAMC。
MD5(message-digest algorithm 5)?(信息-摘要算法)縮寫,廣泛用于加密和解密技術(shù),常用于文件校驗(yàn)。校驗(yàn)?不管文件多大,經(jīng)過(guò)MD5后都能生成唯一的MD5值
SHA (Secure Hash Algorithm,安全散列算法),數(shù)字簽名等密碼學(xué)應(yīng)用中重要的工具,安全性高于MD5
HMAC (Hash Message Authentication Code),散列消息鑒別碼,基于密鑰的Hash算法的認(rèn)證協(xié)議。用公開函數(shù)和密鑰產(chǎn)生一個(gè)固定長(zhǎng)度的值作為認(rèn)證標(biāo)識(shí),用這個(gè)標(biāo)識(shí)鑒別消息的完整性。常用于接口簽名驗(yàn)證
Payload
載荷就是存放有效信息的地方。
有效信息包含三個(gè)部分
1.標(biāo)準(zhǔn)中注冊(cè)的聲明
2.公共的聲明
3.私有的聲明
標(biāo)準(zhǔn)中注冊(cè)的聲明 (建議但不強(qiáng)制使用) :
iss: jwt簽發(fā)者
sub: 面向的用戶(jwt所面向的用戶)
aud: 接收jwt的一方
exp: 過(guò)期時(shí)間戳(jwt的過(guò)期時(shí)間,這個(gè)過(guò)期時(shí)間必須要大于簽發(fā)時(shí)間)
nbf: 定義在什么時(shí)間之前,該jwt都是不可用的.
iat: jwt的簽發(fā)時(shí)間
jti: jwt的唯一身份標(biāo)識(shí),主要用來(lái)作為一次性token,從而回避重放攻擊。
公共的聲明 :
公共的聲明可以添加任何的信息,一般添加用戶的相關(guān)信息或其他業(yè)務(wù)需要的必要信息.但不建議添加敏感信息,因?yàn)樵摬糠衷诳蛻舳丝山饷?
私有的聲明 :
私有聲明是提供者和消費(fèi)者所共同定義的聲明,一般不建議存放敏感信息,因?yàn)閎ase64是對(duì)稱解密的,意味著該部分信息可以歸類為明文信息。
Signature
jwt的第三部分是一個(gè)簽證信息,這個(gè)簽證信息由三部分組成:
header (base64后的)
payload (base64后的)
secret
這個(gè)部分需要base64加密后的header和base64加密后的payload使用.連接組成的字符串,然后通過(guò)header中聲明的加密方式進(jìn)行加鹽secret組合加密,然后就構(gòu)成了jwt的第三部分。
密鑰secret是保存在服務(wù)端的,服務(wù)端會(huì)根據(jù)這個(gè)密鑰進(jìn)行生成token和進(jìn)行驗(yàn)證,所以需要保護(hù)好。
VUE+JWT
在請(qǐng)求攔截器中配置請(qǐng)求頭中攜帶token
// request interceptor service.interceptors.request.use(config => {config.url += '?date=' + new Date().getTime()// 如果是登錄和注冊(cè)的請(qǐng)求,就不需要加JWTif (config.url.indexOf("login") == -1 && config.url.indexOf("register") == -1) {const JWT = sessionStorage.getItem("EPDP_session");config.headers['token'] = JWT}return config},error => {// do something with request errorconsole.log(error) // for debugreturn Promise.reject(error)} )Springboot+JWT
(1)引入JWT依賴,由于是基于Java,所以需要的是java-jwt
<dependency><groupId>com.auth0</groupId><artifactId>java-jwt</artifactId><version>3.4.0</version> </dependency>(2)編寫兩個(gè)注解
用來(lái)跳過(guò)驗(yàn)證的PassToken
@Target({ElementType.METHOD, ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) public @interface PassToken {boolean required() default true; }需要登錄才能進(jìn)行操作的注解UserLoginToken
@Target({ElementType.METHOD, ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) public @interface UserLoginToken {boolean required() default true; }(3)簡(jiǎn)單定義三個(gè)controller
@PassToken@RequestMapping("/register")public Map<String, Object> Register(String usrname, String password1, String password2) throws IOException {Map<String, Object> map = new HashMap<>();String message = "注冊(cè)成功";//密碼不一致if (!password1.equals(password2)) {message = "密碼不一致";map = new HashMap<>();map.put("message", message);return map;}HBaseUsr hBaseUsr = new HBaseUsr();Usr usr = hBaseUsr.SelectFromTable(usrname);//賬戶名已存在if (usr.getUsrname().equals(usrname)) {message = "賬戶名已存在";map = new HashMap<>();map.put("message", message);return map;}try {//初始化項(xiàng)目信息表others:projects,將用戶插入到項(xiàng)目信息表projectService.initUsrProject(usrname);//建立用戶模型歷史記錄表usr_history:usrnamemodelHistoryService.initUsrHistory(usrname);//建立用戶模型表usr_model:usrnameHBaseModel hBaseModel = new HBaseModel(usrname);hBaseModel.CreateUsrModelTable();//建立用戶模型任務(wù)表usr_task:usrnamemodelTaskService.initModelTask(usrname);} catch (Exception e) {e.printStackTrace();throw new MyException("20000","用戶信息添加失敗,請(qǐng)稍后重試","/usr/register");}//插入hBaseUsr表usr.setUsrname(usrname);usr.setPassword(password1);hBaseUsr.InsertToTable(usr);message = "success";map = new HashMap<>();map.put("message", message);return map;}@PassToken@RequestMapping("/login")public ResultBean Login(String usrname, String password) throws Exception {String password_desEncrypt = AES_encryption.desEncrypt(new String(password.getBytes(), "UTF-8")).trim();Map<String, Object> map = new HashMap<>();try {HBaseUsr hBaseUsr = new HBaseUsr();Usr usr = hBaseUsr.SelectFromTable(usrname);//賬戶名不存在if (!usr.getUsrname().equals(usrname)) {return ResultBean.failed("賬戶名不存在");}//密碼不一致else if (!usr.getPassword().equals(password_desEncrypt)) { // else if (!usr.getPassword().equals(password)) {return ResultBean.failed("密碼不一致");}else {//登陸成功map = new HashMap<>();String token = tokenService.getToken(usr);map.put("token", token);}} catch (IOException e) {e.printStackTrace();}return ResultBean.success(map);}@UsrLoginToken@GetMapping("/showInfo")public Map<String, Object> ShowInfo(String usrname) {Map<String, Object> map = new HashMap<>();try {HBaseUsr hBaseUsr = new HBaseUsr();Usr usr = hBaseUsr.SelectFromTable(usrname);map = new HashMap<>();map.put("data", usr);} catch (IOException e) {e.printStackTrace();}return map;}(4)配置攔截器去攔截token并獲取token
攔截器配置
package springapplication.interceptor;import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;/*** @author fourier* @date 2018-07-08 22:33*/ @Configuration public class InterceptorConfig implements WebMvcConfigurer {@Overridepublic void addInterceptors(InterceptorRegistry registry) {registry.addInterceptor(authenticationInterceptor()).addPathPatterns("/**"); // 攔截所有請(qǐng)求,通過(guò)判斷是否有 @LoginRequired 注解 決定是否需要登錄}@Beanpublic AuthenticationInterceptor authenticationInterceptor() {return new AuthenticationInterceptor();} }從請(qǐng)求頭中獲取token并處理
package springapplication.interceptor;import com.auth0.jwt.JWT; import com.auth0.jwt.JWTVerifier; import com.auth0.jwt.algorithms.Algorithm; import com.auth0.jwt.exceptions.JWTDecodeException; import com.auth0.jwt.exceptions.JWTVerificationException; import org.springframework.web.method.HandlerMethod; import org.springframework.web.servlet.HandlerInterceptor; import org.springframework.web.servlet.ModelAndView; import springapplication.annotation.PassToken; import springapplication.annotation.UsrLoginToken; import springapplication.exception.MyException; import springapplication.hdfscore.HBaseUsr; import springapplication.pojo.Usr; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.lang.reflect.Method;/*** @author fourier* @date 2018-07-08 20:41*/ public class AuthenticationInterceptor implements HandlerInterceptor {@Overridepublic boolean preHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object object) throws Exception {String token = httpServletRequest.getHeader("token");// 從 http 請(qǐng)求頭中取出 token// 如果不是映射到方法直接通過(guò)if(!(object instanceof HandlerMethod)){return true;}HandlerMethod handlerMethod=(HandlerMethod)object;Method method=handlerMethod.getMethod();//檢查是否有passtoken注釋,有則跳過(guò)認(rèn)證if (method.isAnnotationPresent(PassToken.class)) {PassToken passToken = method.getAnnotation(PassToken.class);if (passToken.required()) {return true;}}//檢查有沒有需要用戶權(quán)限的注解if (method.isAnnotationPresent(UsrLoginToken.class)) {UsrLoginToken userLoginToken = method.getAnnotation(UsrLoginToken.class);if (userLoginToken.required()) {// 執(zhí)行認(rèn)證if (token == null) {throw new MyException("50000","無(wú)token,請(qǐng)重新登錄","AuthenticationInterceptor");}// 獲取 token 中的 usrnameString usrname;try {usrname = JWT.decode(token).getAudience().get(0);} catch (JWTDecodeException j) {throw new MyException("50000","token錯(cuò)誤,請(qǐng)重新登錄","AuthenticationInterceptor");}HBaseUsr hBaseUsr = new HBaseUsr();Usr usr = hBaseUsr.SelectFromTable(usrname);if (usr == null) {throw new MyException("50000","用戶不存在,請(qǐng)重新登錄","AuthenticationInterceptor");}// 驗(yàn)證 tokenJWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256(usr.getPassword())).build();try {jwtVerifier.verify(token);} catch (JWTVerificationException e) {throw new MyException("50000","token錯(cuò)誤,請(qǐng)重新登錄","AuthenticationInterceptor");}return true;}}return true;}@Overridepublic void postHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, ModelAndView modelAndView) throws Exception {}@Overridepublic void afterCompletion(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, Exception e) throws Exception {} }測(cè)試的話推薦使用Postman
?
與50位技術(shù)專家面對(duì)面20年技術(shù)見證,附贈(zèng)技術(shù)全景圖總結(jié)
以上是生活随笔為你收集整理的VUE+SpringBoot+JWT实现token验证,SSO单点登录的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 原来热加载如此简单,手动写一个 Java
- 下一篇: 面试官扎心一问:Tomcat 在 Spr