項目里一直用的是 spring-security ,不得不說,spring-security 真是東西太多了,學習難度太大(可能我比較菜),這篇博客來總結一下折騰shiro的成果,分享給大家,強烈推薦shiro,真心簡單 : )
引入依賴
1
2
3
4
5
|
<dependency> <groupid>org.apache.shiro</groupid> <artifactid>shiro-spring</artifactid> <version> 1.4 . 0 </version> </dependency> |
用戶,角色,權限
就是經典的rbac權限系統,下面簡單給一下實體類字段
adminuser.java
1
2
3
4
5
6
7
8
9
|
public class adminuser implements serializable { private static final long serialversionuid = 8264158018518861440l; private integer id; private string username; private string password; private integer roleid; // getter setter... } |
role.java
1
2
3
4
5
6
|
public class role implements serializable { private static final long serialversionuid = 7824693669858106664l; private integer id; private string name; // getter setter... } |
permission.java
1
2
3
4
5
6
7
8
9
|
public class permission implements serializable { private static final long serialversionuid = -2694960432845360318l; private integer id; private string name; private string value; // 權限的父節點的id private integer pid; // getter setter... } |
自定義realm
這貨就是查詢用戶的信息然后放在shiro的個人用戶對象的緩存里,shiro自己有一個session的對象(不是servlet里的session)作用就是后面用戶發起請求的時候拿來判斷有沒有權限
另一個作用是查詢一下用戶的信息,將用戶名,密碼組裝成一個 authenticationinfo 用于后面密碼校驗的
具體代碼如下
myshirorealm.java
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
|
@component public class myshirorealm extends authorizingrealm { private logger log = loggerfactory.getlogger(myshirorealm. class ); @autowired private adminuserservice adminuserservice; @autowired private roleservice roleservice; @autowired private permissionservice permissionservice; // 用戶權限配置 @override protected authorizationinfo dogetauthorizationinfo(principalcollection principals) { //訪問@requirepermission注解的url時觸發 simpleauthorizationinfo simpleauthorizationinfo = new simpleauthorizationinfo(); adminuser adminuser = adminuserservice.selectbyusername(principals.tostring()); //獲得用戶的角色,及權限進行綁定 role role = roleservice.selectbyid(adminuser.getroleid()); // 其實這里也可以不要權限那個類了,直接用角色這個類來做鑒權, // 不過角色包含很多的權限,已經算是大家約定的了,所以下面還是查詢權限然后放在authorizationinfo里 simpleauthorizationinfo.addrole(role.getname()); // 查詢權限 list<permission> permissions = permissionservice.selectbyroleid(adminuser.getroleid()); // 將權限具體值取出來組裝成一個權限string的集合 list<string> permissionvalues = permissions.stream().map(permission::getvalue).collect(collectors.tolist()); // 將權限的string集合添加進authorizationinfo里,后面請求鑒權有用 simpleauthorizationinfo.addstringpermissions(permissionvalues); return simpleauthorizationinfo; } // 組裝用戶信息 @override protected authenticationinfo dogetauthenticationinfo(authenticationtoken token) throws authenticationexception { string username = (string) token.getprincipal(); log.info( "用戶:{} 正在登錄..." , username); adminuser adminuser = adminuserservice.selectbyusername(username); // 如果用戶不存在,則拋出未知用戶的異常 if (adminuser == null ) throw new unknownaccountexception(); return new simpleauthenticationinfo(username, adminuser.getpassword(), getname()); } } |
實現密碼校驗
shiro內置了幾個密碼校驗的類,有 md5credentialsmatcher sha1credentialsmatcher , 不過從1.1版本開始,都開始使用 hashedcredentialsmatcher 這個類了,通過配置加密規則來校驗
它們都實現了一個接口 credentialsmatcher 我這里也實現這個接口,實現一個自己的密碼校驗
說明一下,我這里用的加密方式是spring-security里的 bcryptpasswordencoder 作的加密,之所以用它,是因為同一個密碼被這貨加密后,密文都不一樣,下面是具體代碼
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
public class mycredentialsmatcher implements credentialsmatcher { @override public boolean docredentialsmatch(authenticationtoken token, authenticationinfo info) { // 大坑!!!!!!!!!!!!!!!!!!! // 明明token跟info兩個對象的里的credentials類型都是object,斷點看到的類型都是 char[] // 但是!!!!! token里轉成string要先強轉成 char[] // 而info里取credentials就可以直接使用 string.valueof() 轉成string // 醉了。。 string rawpassword = string.valueof(( char []) token.getcredentials()); string encodedpassword = string.valueof(info.getcredentials()); return new bcryptpasswordencoder().matches(rawpassword, encodedpassword); } } |
配置shiro
因為項目是spring-boot開發的,shiro就用java代碼配置,不用xml配置, 具體配置如下
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
|
@configuration public class shiroconfig { private logger log = loggerfactory.getlogger(shiroconfig. class ); @autowired private myshirorealm myshirorealm; @bean public shirofilterfactorybean shirofilter(securitymanager securitymanager) { log.info( "開始配置shirofilter..." ); shirofilterfactorybean shirofilterfactorybean = new shirofilterfactorybean(); shirofilterfactorybean.setsecuritymanager(securitymanager); //攔截器. map<string,string> map = new hashmap<>(); // 配置不會被攔截的鏈接 順序判斷 相關靜態資源 map.put( "/static/**" , "anon" ); //配置退出 過濾器,其中的具體的退出代碼shiro已經替我們實現了 map.put( "/admin/logout" , "logout" ); //<!-- 過濾鏈定義,從上向下順序執行,一般將/**放在最為下邊 -->:這是一個坑呢,一不小心代碼就不好使了; //<!-- authc:所有url都必須認證通過才可以訪問; anon:所有url都都可以匿名訪問--> map.put( "/admin/**" , "authc" ); // 如果不設置默認會自動尋找web工程根目錄下的"/login.jsp"頁面 shirofilterfactorybean.setloginurl( "/adminlogin" ); // 登錄成功后要跳轉的鏈接 shirofilterfactorybean.setsuccessurl( "/admin/index" ); //未授權界面; shirofilterfactorybean.setunauthorizedurl( "/error" ); shirofilterfactorybean.setfilterchaindefinitionmap(map); return shirofilterfactorybean; } // 配置加密方式 // 配置了一下,這貨就是驗證不過,,改成手動驗證算了,以后換加密方式也方便 @bean public mycredentialsmatcher mycredentialsmatcher() { return new mycredentialsmatcher(); } // 安全管理器配置 @bean public securitymanager securitymanager() { defaultwebsecuritymanager securitymanager = new defaultwebsecuritymanager(); myshirorealm.setcredentialsmatcher(mycredentialsmatcher()); securitymanager.setrealm(myshirorealm); return securitymanager; } } |
登錄
都配置好了,就可以發起登錄請求做測試了,一個簡單的表單即可,寫在controller里就行
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
@postmapping ( "/adminlogin" ) public string adminlogin(string username, string password, @requestparam (defaultvalue = "0" ) boolean rememberme, redirectattributes redirectattributes) { try { // 添加用戶認證信息 subject subject = securityutils.getsubject(); if (!subject.isauthenticated()) { usernamepasswordtoken token = new usernamepasswordtoken(username, password, rememberme); //進行驗證,這里可以捕獲異常,然后返回對應信息 subject.login(token); } } catch (authenticationexception e) { // e.printstacktrace(); log.error(e.getmessage()); redirectattributes.addflashattribute( "error" , "用戶名或密碼錯誤" ); redirectattributes.addflashattribute( "username" , username); return redirect( "/adminlogin" ); } return redirect( "/admin/index" ); } |
從上面代碼可以看出,記住我功能也直接都實現好了,只需要在組裝 usernamepasswordtoken 的時候,將記住我字段傳進去就可以了,值是 true, false, 如果是true,登錄成功后,shiro會在本地寫一個cookie
調用 subject.login(token); 方法后,它會去鑒權,期間會產生各種各樣的異常,有以下幾種,可以通過捕捉不同的異常然后提示頁面不同的錯誤信息,相當的方便呀,有木有
- accountexception 帳戶異常
- concurrentaccessexception 這個好像是并發異常
- credentialsexception 密碼校驗異常
- disabledaccountexception 帳戶被禁異常
- excessiveattemptsexception 嘗試登錄次數過多異常
- expiredcredentialsexception 認證信息過期異常
- incorrectcredentialsexception 密碼不正確異常
- lockedaccountexception 帳戶被鎖定異常
- unknownaccountexception 未知帳戶異常
- unsupportedtokenexception login(authenticationtoken) 這個方法只能接收 authenticationtoken 類的對象,如果傳的是其它的類,就拋這個異常
上面這么多異常,shiro在處理登錄的邏輯時,會自動的發出一些異常,當然你也可以手動去處理登錄流程,然后根據不同的問題拋出不同的異常,手動處理的地方就在自己寫的 myshirorealm 里的 dogetauthenticationinfo() 方法里,我在上面代碼里只處理了一個帳戶不存在時拋出了一個 unknownaccountexception 的異常,其實還可以加更多其它的異常,這個要看個人系統的需求來定了
到這里已經可以正常的實現登錄了,下面來說一些其它相關的功能的實現
自定freemarker標簽
開發項目肯定要用到頁面模板,我這里用的是 freemarker ,一個用戶登錄后,頁面可能要根據用戶的不同權限渲染不同的菜單,github上有個開源的庫,也是可以用的,不過我覺得那個太麻煩了,就自己實現了一個,幾行代碼就能搞定
shirotag.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
@component public class shirotag { // 判斷當前用戶是否已經登錄認證過 public boolean isauthenticated(){ return securityutils.getsubject().isauthenticated(); } // 獲取當前用戶的用戶名 public string getprincipal() { return (string) securityutils.getsubject().getprincipal(); } // 判斷用戶是否有 xx 角色 public boolean hasrole(string name) { return securityutils.getsubject().hasrole(name); } // 判斷用戶是否有 xx 權限 public boolean haspermission(string name) { return !stringutils.isempty(name) && securityutils.getsubject().ispermitted(name); } } |
將這個類注冊到freemarker的全局變量里
freemarkerconfig.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
@configuration public class freemarkerconfig { private logger log = loggerfactory.getlogger(freemarkerconfig. class ); @autowired private shirotag shirotag; @postconstruct public void setsharedvariable() throws templatemodelexception { //注入全局配置到freemarker log.info( "開始配置freemarker全局變量..." ); // shiro鑒權 configuration.setsharedvariable( "sec" , shirotag); log.info( "freemarker自定義標簽配置完成!" ); } } |
有了這些配置后,就可以在頁面里使用了,具體用法如下
1
2
3
4
5
6
7
8
|
<# if sec.haspermission( "topic:list" )> <li <# if page_tab== 'topic' > class = "active" </# if >> <a href= "/admin/topic/list" rel= "external nofollow" > <i class = "fa fa-list" ></i> <span>話題列表</span> </a> </li> </# if > |
加上這個后,在渲染頁面的時候,就會根據當前用戶是否有查看話題列表的權限,然后來渲染這個菜單
注解權限
有了上面freemarker標簽判斷是否有權限來渲染頁面,這樣做只能防君子,不能防小人,如果一個人知道后臺的某個訪問鏈接,但這個鏈接它是沒有權限訪問的,那他只要手動輸入這個鏈接就還是可以訪問的,所以這里還要在controller層加一套防御,具體配置如下
在shiroconfig里加上兩個bean
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
//加入注解的使用,不加入這個注解不生效 @bean public authorizationattributesourceadvisor authorizationattributesourceadvisor(defaultwebsecuritymanager securitymanager) { authorizationattributesourceadvisor authorizationattributesourceadvisor = new authorizationattributesourceadvisor(); authorizationattributesourceadvisor.setsecuritymanager(securitymanager); return authorizationattributesourceadvisor; } @bean @conditionalonmissingbean public defaultadvisorautoproxycreator defaultadvisorautoproxycreator() { defaultadvisorautoproxycreator defaultaap = new defaultadvisorautoproxycreator(); defaultaap.setproxytargetclass( true ); return defaultaap; } |
有了這兩個bean就可以用shiro的注解鑒權了,用法如下 @requirespermissions("topic:list")
1
2
3
4
5
6
7
8
9
10
11
|
@controller @requestmapping ( "/admin/topic" ) public class topicadmincontroller extends baseadmincontroller { @requirespermissions ( "topic:list" ) @getmapping ( "/list" ) public string list() { // todo return "admin/topic/list" ; } } |
shiro除了 @requirespermissions 注解外,還有其它幾個鑒權的注解
- @requirespermissions
- @requiresroles
- @requiresuser
- @requiresguest
- @requiresauthentication
一般 @requirespermissions 就夠用了
總結
spring-boot 集成 shiro 到這就結束了,是不是網上能找到的教程里最全的!相比 spring-security 要簡單太多了,強烈推薦
原文鏈接:https://tomoya92.github.io/2018/12/05/spring-boot-shiro/