點(diǎn)擊上方藍(lán)色字體屡萤,關(guān)注我們
這篇文章我們來(lái)學(xué)習(xí)如何使用Spring Boot集成Apache Shiro蔚约。安全應(yīng)該是互聯(lián)網(wǎng)公司的一道生命線(xiàn)娇澎,幾乎任何的公司都會(huì)涉及到這方面的需求逐工。這篇文章會(huì)先介紹一下Apache Shiro描融,在結(jié)合Spring Boot給出使用案例铝噩。
對(duì)于一個(gè)真正為其存在提供良好案例的框架,以及因此您使用它的理由窿克,它應(yīng)該滿(mǎn)足其他替代方案無(wú)法滿(mǎn)足的需求骏庸。為了理解這一點(diǎn)毛甲,我們需要了解Shiro的歷史以及創(chuàng)建時(shí)的替代方案。
在2008年進(jìn)入Apache軟件基金會(huì)之前具被,Shiro已經(jīng)有5年的歷史玻募,之前被稱(chēng)為JSecurity項(xiàng)目,該項(xiàng)目始于2003年初一姿。2003年七咧,Java應(yīng)用程序開(kāi)發(fā)人員的通用安全替代方案并不多 - Java認(rèn)證和授權(quán)服務(wù),也稱(chēng)為JAAS叮叹。JAAS存在許多缺點(diǎn) - 雖然其身份驗(yàn)證功能在某種程度上是可以容忍的艾栋,但授權(quán)方面使用起來(lái)很麻煩且令人沮喪。此外蛉顽,JAAS嚴(yán)重依賴(lài)于虛擬機(jī)級(jí)安全性問(wèn)題蝗砾,例如,確定是否應(yīng)允許在JVM中加載類(lèi)携冤。作為一名應(yīng)用程序開(kāi)發(fā)人員悼粮,我更關(guān)心應(yīng)用程序最終用戶(hù)可以做什么,而不是我的代碼可以在JVM中做什么噪叙。
由于我當(dāng)時(shí)正在使用的應(yīng)用程序矮锈,我還需要訪(fǎng)問(wèn)一個(gè)干凈的霉翔,與容器無(wú)關(guān)的會(huì)話(huà)機(jī)制睁蕾。當(dāng)時(shí)游戲中唯一的會(huì)話(huà)選擇是HttpSessions,它需要一個(gè)Web容器债朵,或EBJ 2.1 Stateful Session Beans子眶,它需要一個(gè)EJB容器。我需要一些可以與容器分離的東西序芦,可以在我選擇的任何環(huán)境中使用臭杰。
最后,存在加密問(wèn)題谚中。有時(shí)候我們都需要保證數(shù)據(jù)安全渴杆,但除非你是加密專(zhuān)家,否則Java密碼體系結(jié)構(gòu)很難理解宪塔。API充滿(mǎn)了檢查異常磁奖,并且使用起來(lái)很麻煩。我希望有一個(gè)更清潔的開(kāi)箱即用的解決方案某筐,可以根據(jù)需要輕松加密和解密數(shù)據(jù)比搭。
因此,從2003年初的安全狀況來(lái)看南誊,您可以很快意識(shí)到在單一身诺,有凝聚力的框架中沒(méi)有任何東西可以滿(mǎn)足所有這些要求蜜托。正因?yàn)槿绱耍琂Security霉赡,以及后來(lái)的Apache Shiro誕生了橄务。
Apache Shiro是一個(gè)功能強(qiáng)大且易于使用的Java安全框架穴亏,可執(zhí)行身份驗(yàn)證仪糖,授權(quán),加密和會(huì)話(huà)管理迫肖,并可用于保護(hù)任何應(yīng)用程序 - 從命令行應(yīng)用程序锅劝,移動(dòng)應(yīng)用程序到最大的Web和企業(yè)應(yīng)用程序。
Authentication(認(rèn)證), Authorization(授權(quán)), Session Management(會(huì)話(huà)管理), Cryptography(加密)被 Shiro 框架的開(kāi)發(fā)團(tuán)隊(duì)稱(chēng)之為應(yīng)用安全的四大基石蟆湖。
Authentication(認(rèn)證):用戶(hù)身份識(shí)別故爵,通常被稱(chēng)為用戶(hù)“登錄”。
Authorization(授權(quán)):訪(fǎng)問(wèn)控制隅津。比如某個(gè)用戶(hù)是否具有某個(gè)操作的使用權(quán)限诬垂。
Session Management(會(huì)話(huà)管理):特定于用戶(hù)的會(huì)話(huà)管理,甚至在非web 或 EJB 應(yīng)用程序。
Cryptography(加密):在對(duì)數(shù)據(jù)源使用加密算法加密的同時(shí)伦仍,保證易于使用结窘。
user_info 用戶(hù)表
sys_user_role 用戶(hù)角色關(guān)聯(lián)表 (一對(duì)多的關(guān)系,一個(gè)角色對(duì)應(yīng)多個(gè)用戶(hù))
sys_permission 權(quán)限表
sys_role_permission 角色權(quán)限關(guān)聯(lián)表 (多對(duì)多關(guān)系)
sys_role 角色表
pom文件
?<dependencies>
????<dependency>
????????<groupId>org.springframework.boot</groupId>
????????<artifactId>spring-boot-starter-data-jpa</artifactId>
????</dependency>
????<dependency>
????????<groupId>org.springframework.boot</groupId>
????????<artifactId>spring-boot-starter-thymeleaf</artifactId>
????</dependency>
????<dependency>
????????<groupId>net.sourceforge.nekohtml</groupId>
????????<artifactId>nekohtml</artifactId>
????????<version>1.9.22</version>
????</dependency>
????<dependency>
????????<groupId>org.springframework.boot</groupId>
????????<artifactId>spring-boot-starter-web</artifactId>
????</dependency>
???<!--?shiro?關(guān)鍵包-->
????<dependency>
????????<groupId>org.apache.shiro</groupId>
????????<artifactId>shiro-spring</artifactId>
????????<version>1.4.0</version>
????</dependency>
????<dependency>
????????<groupId>mysql</groupId>
????????<artifactId>mysql-connector-java</artifactId>
????????<scope>runtime</scope>
????</dependency>
</dependencies>
配置文件
spring.datasource.url=jdbc:mysql://localhost:3306/test??
serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8
spring.datasource.username=root
spring.datasource.password=root
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.jpa.properties.hibernate.hbm2ddl.auto=update
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL5InnoDBDialect
spring.jpa.show-sql=?true
spring.thymeleaf.cache=false
創(chuàng)建用戶(hù)類(lèi)
@Entity
public?class?UserInfo?implements?Serializable?{
??????@Id
??????@GeneratedValue
??????private?Integer?uid;
??????@Column(unique?=true)
??????private?String?username;//帳號(hào)
??????private?String?name;//名稱(chēng)(昵稱(chēng)或者真實(shí)姓名充蓝,不同系統(tǒng)不同定義)
??????private?String?password;?//密碼;
??????private?String?salt;//加密密碼的鹽
??????private?byte?state;//用戶(hù)狀態(tài),0:創(chuàng)建未認(rèn)證(比如沒(méi)有激活隧枫,沒(méi)有輸入驗(yàn)證碼等等)--等待驗(yàn)證的用戶(hù)?,?1:正常狀態(tài),2:用戶(hù)被鎖定.
??????@ManyToMany(fetch=?FetchType.EAGER)//立即從數(shù)據(jù)庫(kù)中進(jìn)行加載數(shù)據(jù);
??????@JoinTable(name?=?"SysUserRole",?joinColumns?=?{?@JoinColumn(name?=?"uid")?},?inverseJoinColumns?={@JoinColumn(name?=?"roleId")?})
??????private?List<SysRole>?roleList;//?一個(gè)用戶(hù)具有多個(gè)角色
??????//?省略setget方法
}
創(chuàng)建角色類(lèi)
@Entity
public?class?SysRole?{
?????@Id@GeneratedValue
?????private?Integer?id;?//?編號(hào)
?????private?String?role;?//?角色標(biāo)識(shí)程序中判斷使用,如"admin",這個(gè)是唯一的:
?????private?String?description;?//?角色描述,UI界面顯示使用
?????private?Boolean?available?=?Boolean.FALSE;?//?是否可用,如果不可用將不會(huì)添加給用戶(hù)
??????//角色?--?權(quán)限關(guān)系:多對(duì)多關(guān)系;
?????@ManyToMany(fetch=?FetchType.EAGER)
?????@JoinTable(name="SysRolePermission",joinColumns={@JoinColumn(name="roleId")},inverseJoinColumns={@JoinColumn(name="permissionId")})
?????private?List<SysPermission>?permissions;
?????//?用戶(hù)?-?角色關(guān)系定義;
????@ManyToMany
????@JoinTable(name="SysUserRole",joinColumns={@JoinColumn(name="roleId")},inverseJoinColumns={@JoinColumn(name="uid")})
?????private?List<UserInfo>?userInfos;//?一個(gè)角色對(duì)應(yīng)多個(gè)用戶(hù)
?????//?省略setget方法
}
權(quán)限類(lèi)
@Entity
public?class?SysPermission?implements?Serializable?{
?????@Id@GeneratedValue
?????private?Integer?id;//主鍵.
?????private?String?name;//名稱(chēng).
?????@Column(columnDefinition="enum('menu','button')")
?????private?String?resourceType;//資源類(lèi)型,[menu|button]
?????private?String?url;//資源路徑.
?????private?String?permission;?//權(quán)限字符串,menu例子:role:*谓苟,button例子:role:create,role:update,role:delete,role:view
?????private?Long?parentId;?//父編號(hào)
?????private?String?parentIds;?//父編號(hào)列表
?????private?Boolean?available?=?Boolean.FALSE;
?????@ManyToMany
?????@JoinTable(name="SysRolePermission",joinColumns={@JoinColumn(name="permissionId")},inverseJoinColumns={@JoinColumn(name="roleId")})
?????private?List<SysRole>?roles;
?????//省略setget方法
}
根據(jù)以上的代碼會(huì)自動(dòng)生成user_info(用戶(hù)信息表)官脓、sys_role(角色表)、sys_permission(權(quán)限表)涝焙、sys_user_role(用戶(hù)角色表)卑笨、sys_role_permission(角色權(quán)限表)這五張表,為了方便測(cè)試我們給這五張表插入一些初始化數(shù)據(jù):
INSERT?INTO?`user_info`?(`uid`,`username`,`name`,`password`,`salt`,`state`)?VALUES?('1',?'admin',?'管理員',?'9c77d6384a1d8a1cc581424e6f0e82d8','root30ea1b94d889ccadeb9f89af63317de2',?0);
INSERT?INTO?`sys_permission`?(`id`,`available`,`name`,`parent_id`,`parent_ids`,`permission`,`resource_type`,`url`)?VALUES?(1,0,'用戶(hù)管理',0,'0/','userInfo:view','menu','userInfo/userList');
INSERT?INTO?`sys_permission`?(`id`,`available`,`name`,`parent_id`,`parent_ids`,`permission`,`resource_type`,`url`)?VALUES?(2,0,'用戶(hù)添加',1,'0/1','userInfo:add','button','userInfo/userAdd');
INSERT?INTO?`sys_permission`?(`id`,`available`,`name`,`parent_id`,`parent_ids`,`permission`,`resource_type`,`url`)?VALUES?(3,0,'用戶(hù)刪除',1,'0/1','userInfo:del','button','userInfo/userDel');
INSERT?INTO?`sys_role`?(`id`,`available`,`description`,`role`)?VALUES?(1,0,'管理員','admin');
INSERT?INTO?`sys_role`?(`id`,`available`,`description`,`role`)?VALUES?(2,0,'VIP會(huì)員','vip');
INSERT?INTO?`sys_role`?(`id`,`available`,`description`,`role`)?VALUES?(3,1,'test','test');
INSERT?INTO?`sys_role_permission`?VALUES?('1',?'1');
INSERT?INTO?`sys_role_permission`?(`permission_id`,`role_id`)?VALUES?(1,1);
INSERT?INTO?`sys_role_permission`?(`permission_id`,`role_id`)?VALUES?(2,1);
INSERT?INTO?`sys_role_permission`?(`permission_id`,`role_id`)?VALUES?(3,2);
INSERT?INTO?`sys_user_role`?(`role_id`,`uid`)?VALUES?(1,1);
Shiro 配置
首先要配置的是ShiroConfig類(lèi)仑撞,Apache Shiro 核心通過(guò) Filter 來(lái)實(shí)現(xiàn)赤兴,就好像SpringMvc 通過(guò)DispachServlet 來(lái)主控制一樣。 既然是使用 Filter 一般也就能猜到隧哮,是通過(guò)URL規(guī)則來(lái)進(jìn)行過(guò)濾和權(quán)限校驗(yàn)桶良,所以我們需要定義一系列關(guān)于URL的規(guī)則和訪(fǎng)問(wèn)權(quán)限。
ShiroConfig
@Configuration
public?class?ShiroConfig?{
@Bean
public?ShiroFilterFactoryBean?shirFilter(SecurityManager?securityManager)?{
????System.out.println("ShiroConfiguration.shirFilter()");
????ShiroFilterFactoryBean?shiroFilterFactoryBean?=?new?ShiroFilterFactoryBean();
????shiroFilterFactoryBean.setSecurityManager(securityManager);
????//攔截器.
????Map<String,String>?filterChainDefinitionMap?=?new?LinkedHashMap<String,String>();
????//?配置不會(huì)被攔截的鏈接?順序判斷
????filterChainDefinitionMap.put("/static/**",?"anon");
????//配置退出?過(guò)濾器,其中的具體的退出代碼Shiro已經(jīng)替我們實(shí)現(xiàn)了
????filterChainDefinitionMap.put("/logout",?"logout");
????//<!--?過(guò)濾鏈定義近迁,從上向下順序執(zhí)行艺普,一般將/**放在最為下邊?-->:這是一個(gè)坑呢,一不小心代碼就不好使了;
????//<!--?authc:所有url都必須認(rèn)證通過(guò)才可以訪(fǎng)問(wèn);?anon:所有url都都可以匿名訪(fǎng)問(wèn)-->
????filterChainDefinitionMap.put("/**",?"authc");
????//?如果不設(shè)置默認(rèn)會(huì)自動(dòng)尋找Web工程根目錄下的"/login.jsp"頁(yè)面
????shiroFilterFactoryBean.setLoginUrl("/login");
????//?登錄成功后要跳轉(zhuǎn)的鏈接
????shiroFilterFactoryBean.setSuccessUrl("/index");
????//未授權(quán)界面;
????shiroFilterFactoryBean.setUnauthorizedUrl("/403");
????shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
????return?shiroFilterFactoryBean;
}
@Bean
public?MyShiroRealm?myShiroRealm(){
????MyShiroRealm?myShiroRealm?=?new?MyShiroRealm();
????return?myShiroRealm;
}
@Bean
public?SecurityManager?securityManager(){
????DefaultWebSecurityManager?securityManager?=??new?DefaultWebSecurityManager();
????securityManager.setRealm(myShiroRealm());
????return?securityManager;
}
}
自定義realm
MyShiroRealm繼承 AuthorizingRealm,重寫(xiě)doGetAuthorizationInfo授權(quán)方法,和doGetAuthenticationInfo認(rèn)證方法。
public?class?MyShiroRealm?extends?AuthorizingRealm?{
@Resource
private?UserInfoService?userInfoService;
@Override
protected?AuthorizationInfo?doGetAuthorizationInfo(PrincipalCollection?principals)?{
????System.out.println("權(quán)限配置-->MyShiroRealm.doGetAuthorizationInfo()");
????SimpleAuthorizationInfo?authorizationInfo?=?new?SimpleAuthorizationInfo();
????UserInfo?userInfo??=?(UserInfo)principals.getPrimaryPrincipal();
????for(SysRole?role:userInfo.getRoleList()){
????????authorizationInfo.addRole(role.getRole());
????????for(SysPermission?p:role.getPermissions()){
????????????authorizationInfo.addStringPermission(p.getPermission());
????????}
????}
????return?authorizationInfo;
}
/*主要是用來(lái)進(jìn)行身份認(rèn)證的歧譬,也就是說(shuō)驗(yàn)證用戶(hù)輸入的賬號(hào)和密碼是否正確岸浑。*/
@Override
protected?AuthenticationInfo?doGetAuthenticationInfo(AuthenticationToken?token)
????????throws?AuthenticationException?{
????System.out.println("MyShiroRealm.doGetAuthenticationInfo()");
????//?獲取用戶(hù)的輸入的賬號(hào).
????String?username?=?(String)token.getPrincipal();
????//?獲取用戶(hù)的輸入的密碼
????System.out.println(token.getCredentials());
????//通過(guò)username從數(shù)據(jù)庫(kù)中查找?User對(duì)象,如果找到瑰步,沒(méi)找到.
????//實(shí)際項(xiàng)目中钞速,這里可以根據(jù)實(shí)際情況做緩存鱼炒,如果不做颈抚,Shiro自己也是有時(shí)間間隔機(jī)制娇未,2分鐘內(nèi)不會(huì)重復(fù)執(zhí)行該方法
????UserInfo?userInfo?=?userInfoService.findByUsername(username);
????System.out.println("----->>userInfo="+userInfo);
????if(userInfo?==?null){
????????return?null;
????}
????//?進(jìn)行認(rèn)證,將正確數(shù)據(jù)給shiro處理
????//?密碼不用自己比對(duì)袁滥,AuthenticationInfo認(rèn)證信息對(duì)象盖桥,一個(gè)接口,new他的實(shí)現(xiàn)類(lèi)對(duì)象SimpleAuthenticationInfo
????/*????第一個(gè)參數(shù)隨便放题翻,可以放user對(duì)象揩徊,程序可在任意位置獲取?放入的對(duì)象
?????*??第二個(gè)參數(shù)必須放密碼,
?????*??第三個(gè)參數(shù)放?當(dāng)前realm的名字嵌赠,因?yàn)榭赡苡卸鄠€(gè)realm*/
????SimpleAuthenticationInfo?authenticationInfo?=?new?SimpleAuthenticationInfo(
????????????userInfo,?//用戶(hù)名
????????????userInfo.getPassword(),?//密碼
????????????ByteSource.Util.bytes(userInfo.getSalt()),
????????????getName()??//realm?name
????);
???//清除之前的授權(quán)信息
????super.clearCachedAuthorizationInfo(authenticationInfo.getPrincipals());
????//?存入用戶(hù)對(duì)象
????SecurityUtils.getSubject().getSession().setAttribute("login",?userInfo);
????//?返回給安全管理器塑荒,securityManager,由securityManager比對(duì)數(shù)據(jù)庫(kù)查詢(xún)出的密碼和頁(yè)面提交的密碼
????//?如果有問(wèn)題姜挺,向上拋異常齿税,一直拋到控制器
????return?authenticationInfo;
}
}
AuthenticationToken
上面定義了接口源碼,主要是兩個(gè)接口炊豪,一個(gè)是獲取委托人信息凌箕,一個(gè)是獲取證明,常用的是用戶(hù)名和密碼的組合溜在。
這里AuthenticationToken只提供接口陌知,一般我們的實(shí)體類(lèi)包含了get/set方法他托,但是這里抽出了get方法掖肋,方便用戶(hù)自己擴(kuò)展所需要的實(shí)現(xiàn)。
public?interface?AuthenticationToken?extends?Serializable?{
???????Object?getPrincipal();
???????Object?getCredentials();
}
其中擴(kuò)展接口HostAuthenticationToken提供了獲取用戶(hù)客戶(hù)host的功能赏参,源代碼如下:
public?interface?HostAuthenticationToken?extends?AuthenticationToken?{
???????String?getHost();
}
RememberMeAuthenticationToken提供了記住用戶(hù)的標(biāo)識(shí):
public?interface?RememberMeAuthenticationToken?extends?AuthenticationToken?{
???????boolean?isRememberMe();
}
登錄過(guò)程其實(shí)只是處理異常的相關(guān)信息志笼,具體的登錄驗(yàn)證交給shiro來(lái)處理。
@Controller
public?class?HomeController?{
@RequestMapping({"/","/index"})
public?String?index(){
?????????return"/index";
}
@RequestMapping("/login")
public?String?login(HttpServletRequest?request,?Map<String,?Object>?map)?throws?Exception{
????System.out.println("HomeController.login()");
????//?登錄失敗從request中獲取shiro處理的異常信息把篓。
????//?shiroLoginFailure:就是shiro異常類(lèi)的全類(lèi)名.
????String?exception?=?(String)?request.getAttribute("shiroLoginFailure");
????System.out.println("exception="?+?exception);
????String?msg?=?"";
????if?(exception?!=?null)?{
????????if?(UnknownAccountException.class.getName().equals(exception))?{
????????????System.out.println("UnknownAccountException?--?>?賬號(hào)不存在:");
????????????msg?=?"UnknownAccountException?--?>?賬號(hào)不存在:";
????????}?else?if?(IncorrectCredentialsException.class.getName().equals(exception))?{
????????????System.out.println("IncorrectCredentialsException?--?>?密碼不正確:");
????????????msg?=?"IncorrectCredentialsException?--?>?密碼不正確:";
????????}?else?if?("kaptchaValidateFailed".equals(exception))?{
????????????System.out.println("kaptchaValidateFailed?--?>?驗(yàn)證碼錯(cuò)誤");
????????????msg?=?"kaptchaValidateFailed?--?>?驗(yàn)證碼錯(cuò)誤";
????????}?else?{
????????????msg?=?"else?>>?"+exception;
????????????System.out.println("else?--?>"?+?exception);
????????}
????}
????map.put("msg",?msg);
????//?此方法不處理登錄成功,由shiro進(jìn)行處理
????return?"/login";
}
@RequestMapping("/403")
public?String?unauthorizedRole(){
????System.out.println("------沒(méi)有權(quán)限-------");
????return?"403";
}
}
其它dao層和service的代碼就不貼出來(lái)了大家直接看代碼纫溃。
登錄頁(yè)面
登錄成功會(huì)跳轉(zhuǎn) index 頁(yè)面,如果錯(cuò)誤會(huì)返回錯(cuò)誤信息韧掩。
上面這些操作時(shí)候觸發(fā)MyShiroRealm.doGetAuthorizationInfo()
這個(gè)方法紊浩,也就是權(quán)限校驗(yàn)的方法。
可以在數(shù)據(jù)庫(kù)中修改不同的權(quán)限進(jìn)行測(cè)試,demo中有對(duì)用戶(hù)的增刪查改坊谁,就不展示了费彼,大家可以下載demo運(yùn)行。
shiro很強(qiáng)大口芍,這僅僅是完成了登錄認(rèn)證和權(quán)限管理箍铲、用戶(hù)管理的增刪查改。
github地址:
https://github.com/xiaonongOne/springboot-shiro