本文介紹了spring-boot結合shrio實現jwt的方法,分享給大家,具體如下:
關于驗證大致分為兩個方面:
- 用戶登錄時的驗證;
- 用戶登錄后每次訪問時的權限認證
主要解決方法:使用自定義的shiro filter
項目搭建:
這是一個spring-boot 的web項目,不了解spring-boot的項目搭建,請google。
pom.mx引入相關jar包
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
<!-- shiro 權限管理 --> <dependency> <groupid>org.apache.shiro</groupid> <artifactid>shiro-spring</artifactid> <version>${shiro.version}</version> </dependency> <dependency> <groupid>org.apache.shiro</groupid> <artifactid>shiro-core</artifactid> <version>${shiro.version}</version> </dependency> <!-- jwt --> <dependency> <groupid>io.jsonwebtoken</groupid> <artifactid>jjwt</artifactid> <version> 0.9 . 0 </version> </dependency> |
shrio 的相關配置
劃重點!!自定義了一個filter
1
|
filtermap.put( "jwtfilter" , new jwtfilter()); |
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
|
@configuration public class shiroconfig { @bean public shirofilterfactorybean getshirofilterfactorybean(securitymanager securitymanager) { shirofilterfactorybean shirofilterfactorybean = new shirofilterfactorybean(); shirofilterfactorybean.setsecuritymanager(securitymanager); // 添加自己的過濾器并且取名為jwtfilter map<string, filter> filtermap = new hashmap<>(); filtermap.put( "jwtfilter" , new jwtfilter()); shirofilterfactorybean.setfilters(filtermap); /* * 自定義url規則 * http://shiro.apache.org/web.html#urls- */ map<string, string> filterchaindefinitionmap = shirofilterfactorybean.getfilterchaindefinitionmap(); filterchaindefinitionmap.put("/**", "jwtfilter"); shirofilterfactorybean.setfilterchaindefinitionmap(filterchaindefinitionmap); return shirofilterfactorybean; } /** * securitymanager 不用直接注入shirodbrealm,可能會導致事務失效 * 解決方法見 handlecontextrefresh * http://www.debugrun.com/a/nks9ejq.html */ @bean("securitymanager") public defaultwebsecuritymanager securitymanager(tokenrealm tokenrealm) { defaultwebsecuritymanager manager = new defaultwebsecuritymanager(); manager.setrealm(tokenrealm); /* * 關閉shiro自帶的session,詳情見文檔 * http://shiro.apache.org/session-management.html#sessionmanagement-statelessapplications%28sessionless%29 */ defaultsubjectdao subjectdao = new defaultsubjectdao(); defaultsessionstorageevaluator defaultsessionstorageevaluator = new defaultsessionstorageevaluator(); defaultsessionstorageevaluator.setsessionstorageenabled( false ); subjectdao.setsessionstorageevaluator(defaultsessionstorageevaluator); manager.setsubjectdao(subjectdao); return manager; } @bean public lifecyclebeanpostprocessor lifecyclebeanpostprocessor() { return new lifecyclebeanpostprocessor(); } @bean (name = "tokenrealm" ) @dependson ( "lifecyclebeanpostprocessor" ) public tokenrealm tokenrealm() { return new tokenrealm(); } @bean @dependson ( "lifecyclebeanpostprocessor" ) public defaultadvisorautoproxycreator defaultadvisorautoproxycreator() { defaultadvisorautoproxycreator defaultadvisorautoproxycreator = new defaultadvisorautoproxycreator(); // 強制使用cglib,防止重復代理和可能引起代理出錯的問題 // https://zhuanlan.zhihu.com/p/29161098 defaultadvisorautoproxycreator.setproxytargetclass( true ); return defaultadvisorautoproxycreator; } @bean public authorizationattributesourceadvisor getauthorizationattributesourceadvisor(securitymanager securitymanager) { authorizationattributesourceadvisor authorizationattributesourceadvisor = new authorizationattributesourceadvisor(); authorizationattributesourceadvisor.setsecuritymanager(securitymanager); return new authorizationattributesourceadvisor(); } } |
自定義shrio filter
執行順序:prehandle -> dofilterinternal -> executelogin -> onloginsuccess
主要判斷是不是登錄請求的是 dofilterinternal
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
|
public class jwtfilter extends basichttpauthenticationfilter { /** * 自定義執行登錄的方法 */ @override protected boolean executelogin(servletrequest request, servletresponse response) throws ioexception { httpservletrequest httpservletrequest = (httpservletrequest) request; usernamepasswordtoken usernamepasswordtoken = json.parseobject(httpservletrequest.getinputstream(), usernamepasswordtoken. class ); // 提交給realm進行登入,如果錯誤他會拋出異常并被捕獲 subject subject = this .getsubject(request, response); subject.login(usernamepasswordtoken); return this .onloginsuccess(usernamepasswordtoken, subject, request, response); //錯誤拋出異常 } /** * 最先執行的方法 */ @override protected boolean prehandle(servletrequest request, servletresponse response) throws exception { return super .prehandle(request, response); } /** * 登錄成功后登錄的操作 * 加上jwt 的header */ @override protected boolean onloginsuccess(authenticationtoken token, subject subject, servletrequest request, servletresponse response) { httpservletresponse httpservletresponse = (httpservletresponse) response; string jwttoken = jwts.builder() .setid(token.getprincipal().tostring()) .setexpiration(datetime.now().plusminutes( 30 ).todate()) .signwith(signaturealgorithm.hs256, jwtcost.signaturekey) .compact(); httpservletresponse.addheader(authorization_header, jwttoken); return true ; } /** * 登錄以及校驗的主要流程 * 判斷是否是登錄,或者是登陸后普通的一次請求 */ @override public void dofilterinternal(servletrequest servletrequest, servletresponse servletresponse, filterchain filterchain) throws ioexception, servletexception { httpservletrequest httpservletrequest = (httpservletrequest) servletrequest; httpservletresponse httpservletresponse = (httpservletresponse) servletresponse; string servletpath = httpservletrequest.getservletpath(); if (stringutils.equals(servletpath, "/login" )) { //執行登錄 this .executelogin(servletrequest, servletresponse); } else { string authenticationheader = httpservletrequest.getheader(authorization_header); if (stringutils.isnotempty(authenticationheader)) { claims body = jwts.parser() .setsigningkey(jwtcost.signaturekey) .parseclaimsjws(authenticationheader) .getbody(); if (body != null ) { //更新token body.setexpiration(datetime.now().plusminutes( 30 ).todate()); string updatetoken = jwts.builder().setclaims(body).compact(); httpservletresponse.addheader(authorization_header, updatetoken); //添加用戶憑證 principalcollection principals = new simpleprincipalcollection(body.getid(), jwtcost.usernamepasswordrealm); //拼裝shiro用戶信息 websubject.builder builder = new websubject.builder(servletrequest, servletresponse); builder.principals(principals); builder.authenticated( true ); builder.sessioncreationenabled( false ); websubject subject = builder.buildwebsubject(); //塞入容器,統一調用 threadcontext.bind(subject); filterchain.dofilter(httpservletrequest, httpservletresponse); } } else { httpservletresponse.setstatus(httpstatus.forbidden.value()); } } } } |
登錄失敗處理
處理shrio異常
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
|
@restcontrolleradvice public class globalcontrollerexceptionhandler { @exceptionhandler (value = exception. class ) public object allexceptionhandler(httpservletrequest request, httpservletresponse response, exception exception) { string message = exception.getcause().getmessage(); logutil.error(message); return new resultinfo(exception.getclass().getname(), message); } /*=========== shiro 異常攔截==============*/ @exceptionhandler (value = incorrectcredentialsexception. class ) public string incorrectcredentialsexception(httpservletrequest request, httpservletresponse response, exception exception) { response.setstatus(httpstatus.forbidden.value()); return "incorrectcredentialsexception" ; } @exceptionhandler (value = unknownaccountexception. class ) public string unknownaccountexception(httpservletrequest request, httpservletresponse response, exception exception) { response.setstatus(httpstatus.forbidden.value()); return "unknownaccountexception" ; } @exceptionhandler (value = lockedaccountexception. class ) public string lockedaccountexception(httpservletrequest request, httpservletresponse response, exception exception) { response.setstatus(httpstatus.forbidden.value()); return "lockedaccountexception" ; } @exceptionhandler (value = excessiveattemptsexception. class ) public string excessiveattemptsexception(httpservletrequest request, httpservletresponse response, exception exception) { response.setstatus(httpstatus.forbidden.value()); return "excessiveattemptsexception" ; } @exceptionhandler (value = authenticationexception. class ) public string authenticationexception(httpservletrequest request, httpservletresponse response, exception exception) { response.setstatus(httpstatus.forbidden.value()); return "authenticationexception" ; } @exceptionhandler (value = unauthorizedexception. class ) public string unauthorizedexception(httpservletrequest request, httpservletresponse response, exception exception) { response.setstatus(httpstatus.forbidden.value()); return "unauthorizedexception" ; } } |
處理jwt異常
這是個坑,因為是在filter內發生的異常,@exceptionhandler是截獲不到的。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
|
/** * 截獲spring boot error頁面 */ @restcontroller public class globalexceptionhandler implements errorcontroller { @override public string geterrorpath() { return "/error" ; } @requestmapping (value = "/error" ) public object error(httpservletrequest request, httpservletresponse response) throws exception { // 錯誤處理邏輯 exception exception = (exception) request.getattribute( "javax.servlet.error.exception" ); throwable cause = exception.getcause(); if (cause instanceof expiredjwtexception) { response.setstatus(httpstatus.gateway_timeout.value()); return new resultinfo( "expiredjwtexception" , cause.getmessage()); } if (cause instanceof malformedjwtexception) { response.setstatus(httpstatus.forbidden.value()); return new resultinfo( "malformedjwtexception" , cause.getmessage()); } return new resultinfo(cause.getcause().getmessage(), cause.getmessage()); } } |
關于權限等授權信息,可以直接放到redis中實現緩存。我認為也是不錯的。
源碼奉上:githup-shiro分支 :溫馨提示:平時測試代碼可能比較亂。
以上就是本文的全部內容,希望對大家的學習有所幫助,也希望大家多多支持服務器之家。
原文鏈接:https://segmentfault.com/a/1190000014750168