作為一個(gè)Apache Shiro小白,最近跟著“純潔的微笑”和“冷豪”等的博客,學(xué)習(xí)了一下Apache Shiro。將自己的一些簡(jiǎn)單理解記錄下來(lái),希望對(duì)你有所幫助。
Apache Shiro在我工作項(xiàng)目中主要用于登陸身份驗(yàn)證和訪問(wèn)權(quán)限控制。工作項(xiàng)目用的是SpringMVC框架,最近學(xué)習(xí)SpringBoot2,我就在SpringBoot2中來(lái)驗(yàn)證一下Apache Shiro。以下Apache Shiro簡(jiǎn)稱Shiro。
一、Shiro登陸架構(gòu)
下面是Shiro的用戶登陸架構(gòu)圖,我們根據(jù)箭頭來(lái)看一下流程。

1、Token:使用用戶的登錄信息創(chuàng)建令牌
UsernamePasswordToken token = new UsernamePasswordToken(username, password, true);
我們要先通過(guò)用戶名和密碼,生成一個(gè)token,token是一個(gè)用戶令牌,用于在登陸的時(shí)候,Shiro來(lái)驗(yàn)證用戶是否有合法的身份。
2、Subject:執(zhí)行登陸動(dòng)作(login)
Subject subject = SecurityUtils.getSubject(); // 獲取Subject單例對(duì)象
subject.login(token); // 登陸
再通過(guò)Subject來(lái)執(zhí)行登陸操作,將token發(fā)送給Security Manager,讓他來(lái)驗(yàn)證這個(gè)token。Subject中文翻譯是主題。你可以理解為它是一個(gè)用戶,是User的抽象概念。
3、Realm:自定義代碼實(shí)現(xiàn)登陸身份驗(yàn)證和訪問(wèn)權(quán)限控制
先來(lái)看看Realm,你從上圖可以看出,Realm在Shiro方框的外面。圖片很形象,因?yàn)檫@一部分恰恰是需要我們自己去實(shí)現(xiàn)的。需要我們來(lái)設(shè)計(jì)如何驗(yàn)證登錄用戶的身份(role),和這個(gè)用戶是否具有訪問(wèn)某個(gè)URL的權(quán)限(permission)。前者使用AuthenticationInfo(驗(yàn)證)實(shí)現(xiàn),后者使用AuthorizationInfo(授權(quán))實(shí)現(xiàn)。
4、Security Manager:Shiro架構(gòu)的核心
Security Manager,是Shiro架構(gòu)的核心,簡(jiǎn)單來(lái)說(shuō),它根據(jù)我們自定義的Realm,去完成驗(yàn)證和授權(quán)工作
如果這部分沒(méi)有看懂,建議先根據(jù)下面的“與SpringBoot2集成”部分,搭建一個(gè)demo,在項(xiàng)目中直觀體驗(yàn)一下了,再回來(lái)看。
二、與SpringBoot2集成
注:以下內(nèi)容是根據(jù)“純潔的微笑”大神的《springboot整合shiro-登錄認(rèn)證和權(quán)限管理》一文,將其中SpringBoot1.5升級(jí)到2.1,針對(duì)2.1做了相應(yīng)修改,同時(shí)針對(duì)一些知識(shí)點(diǎn)延伸學(xué)習(xí)。博文地址:http://www.ityouknow.com/springboot/2017/06/26/springboot-shiro.html
建議你手動(dòng)搭建一個(gè)demo,這樣能更深入了解。
pom依賴
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!--web核心-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--thymeleaf模板-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<!--HTML掃描器和標(biāo)簽補(bǔ)償器,補(bǔ)充thymeleaf對(duì)html的嚴(yán)格檢驗(yàn)-->
<dependency>
<groupId>net.sourceforge.nekohtml</groupId>
<artifactId>nekohtml</artifactId>
<version>1.9.22</version>
</dependency>
<!--Apache Shiro-->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.4.0</version>
</dependency>
<!--使用Spring Data JPA和Hibernate-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<!--用于MySQL的JDBC Type 4驅(qū)動(dòng)程序-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<!--熱部署-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<optional>true</optional>
</dependency>
<!--可使用注解自動(dòng)生成getter、setter等方法-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.4</version>
</dependency>
</dependencies>
<!--為使用熱部署,配置<build></build>-->
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<fork>true</fork>
</configuration>
</plugin>
</plugins>
</build>
配置文件
spring:
datasource:
url: jdbc:mysql://localhost:3306/springboot_shiro?useUnicode=true&characterEncoding=utf-8&serverTimezone=UTC
username: root
password: 12345678
driver-class-name: com.mysql.cj.jdbc.Driver
thymeleaf:
cache: false #禁用模板引擎編譯的緩存結(jié)果。由熱部署來(lái)實(shí)現(xiàn),更改代碼后,使用Ctrl+F9(IDEA)更新
mode: LEGACYHTML5 #避免thymeleaf對(duì)html文件的嚴(yán)格校驗(yàn)(如檢查標(biāo)簽必須對(duì)稱等)
#使用jpa技術(shù),運(yùn)行實(shí)體代碼自動(dòng)生成數(shù)據(jù)表
jpa:
database: mysql
show-sql: true
hibernate:
ddl-auto: update
properties:
hibernate:
dialect: org.hibernate.dialect.MySQL5Dialect
server:
port: 9090
數(shù)據(jù)庫(kù)設(shè)計(jì)
使用基于角色的訪問(wèn)控制(Role-Based Access Control)---RBAC 來(lái)實(shí)現(xiàn)數(shù)據(jù)庫(kù)設(shè)計(jì),用戶依賴角色,角色依賴權(quán)限。這樣設(shè)計(jì)結(jié)構(gòu)清晰,管理方便。建立三張表:user_info,sys_role,sys_permission。使用sys_user_role關(guān)聯(lián)用戶和角色,使用sys_role_permission關(guān)聯(lián)角色和權(quán)限,不使用外鍵。
使用jpa技術(shù),運(yùn)行實(shí)體代碼自動(dòng)生成數(shù)據(jù)表。
用戶信息實(shí)體。@Getter、@Setter注解用于提供讀寫(xiě)屬性。因?yàn)橛術(shù)etCredentialsSalt(),所以不使用@Data注解。
@Entity
@Getter
@Setter
public class UserInfo implements Serializable {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)//GenerationType.IDENTITY避免生成hibernate_sequence表
private Integer uid;
@Column(unique = true)
private String username;//帳號(hào)
private String name;//名稱(昵稱或者真實(shí)姓名,不同系統(tǒng)不同定義)
private String password; //密碼;
private String salt;//加密密碼的鹽
private byte state;//用戶狀態(tài),0:創(chuàng)建未認(rèn)證(比如沒(méi)有激活,沒(méi)有輸入驗(yàn)證碼等等)--等待驗(yàn)證的用戶 , 1:正常狀態(tài),2:用戶被鎖定.
@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è)用戶具有多個(gè)角色
/**
* 密碼鹽,重新對(duì)鹽重新進(jìn)行了定義,用戶名+salt,這樣就更加不容易被破解
*
* @return
*/
public String getCredentialsSalt() {
return this.username + this.salt;
}
}
角色實(shí)體。使用@Data注解,為類提供讀寫(xiě)屬性, 此外還提供了 equals()、hashCode()、toString() 方法。上面用戶信息實(shí)體和角色實(shí)體會(huì)根據(jù)@JoinTable注解生成sys_user_role表。
@Entity
@Data
public class SysRole {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id; // 編號(hào)
private String role; // 角色標(biāo)識(shí)程序中判斷使用,如"admin",這個(gè)是唯一的:
private String description; // 角色描述,UI界面顯示使用
private Boolean available = Boolean.FALSE; // 是否可用,如果不可用將不會(huì)添加給用戶
// 用戶 - 角色關(guān)系定義;
@ManyToMany
@JoinTable(name = "SysUserRole", joinColumns = {@JoinColumn(name = "roleId")}, inverseJoinColumns = {@JoinColumn(name = "uid")})
private List<UserInfo> userInfos;// 一個(gè)角色對(duì)應(yīng)多個(gè)用戶
//角色 -- 權(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;
}
權(quán)限實(shí)體。同理,角色實(shí)體和權(quán)限實(shí)體,通過(guò)@JoinTable注解生成sys_role_permission表
@Entity
@Data
public class SysPermission {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;//主鍵.
private String name;//名稱.
@Column(columnDefinition = "enum('menu','button')")
private String resourceType;//資源類型,[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;
}
數(shù)據(jù)庫(kù)數(shù)據(jù):
INSERT INTO `user_info` (`uid`,`username`,`name`,`password`,`salt`,`state`) VALUES ('1', 'admin', '管理員', 'd3c59d25033dbf980d29554025c23a75', '8d78869f470951332959580424d4bf4f', 0);
INSERT INTO `sys_permission` (`id`,`available`,`name`,`parent_id`,`parent_ids`,`permission`,`resource_type`,`url`) VALUES (1,0,'用戶管理',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,'用戶添加',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,'用戶刪除',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 (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
Apache Shiro 核心通過(guò) Filter 來(lái)實(shí)現(xiàn),就好像SpringMvc 通過(guò)DispachServletqu去實(shí)現(xiàn)。
Filter和Interceptor的區(qū)別:
Filter是過(guò)濾器,Interceptor是攔截器。前者基于回調(diào)函數(shù)實(shí)現(xiàn),必須依靠容器支持。因?yàn)樾枰萜餮b配好整條FilterChain并逐個(gè)調(diào)用。后者基于代理實(shí)現(xiàn),屬于AOP的范疇。
在Shrio中實(shí)現(xiàn)登陸身份驗(yàn)證和訪問(wèn)權(quán)限控制有三種方式:
- 1、完全使用注解來(lái)實(shí)現(xiàn)登陸身份驗(yàn)證和訪問(wèn)權(quán)限控制
- 2、完全使用URL配置來(lái)實(shí)現(xiàn)登陸身份驗(yàn)證和訪問(wèn)權(quán)限控制
- 3、使用URL配置來(lái)實(shí)現(xiàn)登陸身份驗(yàn)證、使用注解來(lái)實(shí)現(xiàn)訪問(wèn)權(quán)限控制
第3種方式最靈活,所以用第三種。
1、使用URL配置來(lái)實(shí)現(xiàn)登陸身份驗(yàn)證
要實(shí)現(xiàn)當(dāng)用戶在瀏覽器地址訪問(wèn)項(xiàng)目URL時(shí),Shiro會(huì)攔截所有的請(qǐng)求,再根據(jù)配置的ShrioFilter過(guò)濾器來(lái)進(jìn)行下一步操作。原理:Spring容器會(huì)將所有的Filter交給ShiroFilter管理。
@Configuration
public class ShiroConfig {
@Bean
public ShiroFilterFactoryBean shiroFilter() {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
shiroFilterFactoryBean.setSecurityManager(securityManager());
Map<String, String> filterChainDefinitionMap = new LinkedHashMap<String, String>();
// 以下過(guò)濾器按順序判斷
// 配置不會(huì)被攔截的鏈接,一般是排除前端文件(anon:指定的url可以匿名訪問(wèn))
// filterChainDefinitionMap.put("/static/**", "anon");
//配置退出 過(guò)濾器,其中的具體的退出代碼Shiro已經(jīng)替我們實(shí)現(xiàn)了
filterChainDefinitionMap.put("/logout", "logout");
//authc:所有url都必須認(rèn)證通過(guò)才可以訪問(wèn);
filterChainDefinitionMap.put("/**", "authc");
//當(dāng)項(xiàng)目訪問(wèn)其他沒(méi)有通過(guò)認(rèn)證的URL時(shí),會(huì)默認(rèn)跳轉(zhuǎn)到/login,如果不設(shè)置默認(rèn)會(huì)自動(dòng)尋找Web工程根目錄下的"/login.jsp"頁(yè)面
shiroFilterFactoryBean.setLoginUrl("/login");
//登錄成功后要跳轉(zhuǎn)的鏈接
shiroFilterFactoryBean.setSuccessUrl("/index");
//當(dāng)用戶訪問(wèn)沒(méi)有權(quán)限的URL時(shí),跳轉(zhuǎn)到未授權(quán)界面
shiroFilterFactoryBean.setUnauthorizedUrl("/403");
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
return shiroFilterFactoryBean;
}
@Bean
public SecurityManager securityManager() {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(myShiroRealm());
return securityManager;
}
@Bean
public MyShiroRealm myShiroRealm() {
MyShiroRealm myShiroRealm = new MyShiroRealm();
myShiroRealm.setCredentialsMatcher(hashedCredentialsMatcher());
}
}
authc更深層次含義:指定url需要form表單登錄,默認(rèn)會(huì)從請(qǐng)求中獲取username、password等參數(shù)并嘗試登錄,如果登錄不了就會(huì)跳轉(zhuǎn)到loginUrl配置的路徑
在Realm中實(shí)現(xiàn)AuthenticationInfo(登陸身份驗(yàn)證)
doGetAuthenticationInfo():用于驗(yàn)證token的User是否具有合法的身份,即檢驗(yàn)賬號(hào)密碼是否正確,每次用戶登錄的時(shí)候都會(huì)調(diào)用。
public class MyShiroRealm extends AuthorizingRealm {
@Autowired
UserService userService;
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
//獲取登錄的用戶名
String username = (String) authenticationToken.getPrincipal();
//根據(jù)用戶名在數(shù)據(jù)庫(kù)中查找此用戶
//實(shí)際項(xiàng)目中,這里可以根據(jù)實(shí)際情況做緩存,如果不做,Shiro自己也是有時(shí)間間隔機(jī)制,2分鐘內(nèi)不會(huì)重復(fù)執(zhí)行該方法
UserInfo userInfo = userService.findByUsername(username);
if (userInfo == null) {
return null;
}
//根據(jù)salt來(lái)驗(yàn)證token中的密碼是否跟從數(shù)據(jù)庫(kù)查找的密碼匹配,匹配則登錄成功。getName()設(shè)置當(dāng)前Realm的唯一名稱,可自定義
return new SimpleAuthenticationInfo(
userInfo,
userInfo.getPassword(),
ByteSource.Util.bytes(userInfo.getCredentialsSalt()),//鹽
getName());
}
}
驗(yàn)證密碼原理
先了解兩個(gè)算法,散列算法與加密算法。
兩者都是將一個(gè)Object變成一串無(wú)意義的字符串,不同點(diǎn)是經(jīng)過(guò)散列的對(duì)象無(wú)法復(fù)原,是一個(gè)單向的過(guò)程。例如,對(duì)密碼的加密通常就是使用散列算法,因此用戶如果忘記密碼只能通過(guò)修改而無(wú)法獲取原始密碼。但是對(duì)于信息的加密則是正規(guī)的加密算法,經(jīng)過(guò)加密的信息是可以通過(guò)秘鑰解密和還原。
在這里,我們將用戶的密碼使用散列算法(MD5)加密后保存到數(shù)據(jù)庫(kù)。加密的時(shí)候就使用了salt,salt中文翻譯是鹽,你可以將他看成一個(gè)鑰匙。
因?yàn)樯⒘兴惴用苁菃雾?xiàng)的,不能還原。那我們?nèi)绾蝸?lái)驗(yàn)證密碼呢,這時(shí)候也需要使用salt。我們將token中的明文密碼,采用生成密文密碼時(shí)一樣的方式,通過(guò)salt再加密一次,對(duì)比兩個(gè)加密后的密碼。最后的驗(yàn)證是SimpleAuthenticationInfo去實(shí)現(xiàn)的。
如何散列加密
//newPassword(密文密碼):d3c59d25033dbf980d29554025c23a75
String newPassword = new SimpleHash("MD5",//散列算法:這里使用MD5算法
"123456",//明文密碼
ByteSource.Util.bytes("admin8d78869f470951332959580424d4bf4f"),//salt:用戶名 + salt
2//散列的次數(shù),相當(dāng)于MD5(MD5(**))
).toHex();
//生成一個(gè)32位數(shù)的salt
byte[] saltByte = new byte[16];
SecureRandom random = new SecureRandom();
random.nextBytes(saltByte);
String salt = Hex.encodeToString((saltByte));
如何驗(yàn)證密碼
配置hashedCredentialsMatcher(憑證匹配器),讓SimpleAuthorizationInfo知道如何驗(yàn)證密碼:
@Bean
public HashedCredentialsMatcher hashedCredentialsMatcher() {
HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
hashedCredentialsMatcher.setHashAlgorithmName("md5");//散列算法
hashedCredentialsMatcher.setHashIterations(2);//散列的次數(shù)
return hashedCredentialsMatcher;
}
2、使用注解來(lái)實(shí)現(xiàn)訪問(wèn)權(quán)限控制
@Controller
@RequestMapping("/userInfo")
public class UserInfoController {
/**
* 用戶查詢;
* @return
*/
@RequestMapping("/userList")
@RequiresPermissions("userInfo:view")//訪問(wèn)的權(quán)限
public String userList(){
return "userInfo";
}
/**
* 用戶添加;
* @return
*/
@RequestMapping("/userAdd")
@RequiresPermissions("userInfo:add")//新增的權(quán)限
public String userAdd(){
return "userInfoAdd";
}
/**
* 用戶刪除;
* @return
*/
@RequestMapping("/userDel")
@RequiresPermissions("userInfo:del")//刪除的權(quán)限
public String userDel(){
return "userInfoDel";
}
}
在Relm中實(shí)現(xiàn)AuthorizationInfo(訪問(wèn)權(quán)限控制)
如果項(xiàng)目只需要Apache Shiro用于登陸驗(yàn)證,那么就不用使用AuthorizationInfo,只需要返回一個(gè)null。
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
return null;
}
doGetAuthorizationInfo():當(dāng)用戶訪問(wèn)帶有@RequiresPermissions注解的URL時(shí),會(huì)調(diào)用此方法驗(yàn)證是否有權(quán)限訪問(wèn)。
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
System.out.println("權(quán)限配置-->MyShiroRealm.doGetAuthorizationInfo()");
UserInfo userInfo = (UserInfo) principalCollection.getPrimaryPrincipal();
SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
//獲取當(dāng)前用戶的角色與權(quán)限,讓simpleAuthorizationInfo去驗(yàn)證
for (SysRole sysRole : userInfo.getRoleList()) {
simpleAuthorizationInfo.addRole(sysRole.getRole());
for (SysPermission sysPermission : sysRole.getPermissions()) {
simpleAuthorizationInfo.addStringPermission(sysPermission.getPermission());
}
}
return simpleAuthorizationInfo;
}
代碼開(kāi)啟注解
使用Shrio注解,需要在ShrioConfig中使用AuthorizationAttributeSourceAdvisor開(kāi)啟
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
return authorizationAttributeSourceAdvisor;
}
自定義異常處理
當(dāng)沒(méi)有訪問(wèn)權(quán)限時(shí),會(huì)拋出異常,需要自定義異常處理,將沒(méi)有權(quán)限的異常重定向到403頁(yè)面
@Bean
public SimpleMappingExceptionResolver
createSimpleMappingExceptionResolver() {
System.out.println("自定義異常處理");
SimpleMappingExceptionResolver resolver = new SimpleMappingExceptionResolver();
Properties mappings = new Properties();
mappings.setProperty("UnauthorizedException", "403");//授權(quán)異常處理
resolver.setExceptionMappings(mappings); // None by default
resolver.setDefaultErrorView("error"); // No default
resolver.setExceptionAttribute("ex"); // Default is "exception"
return resolver;
}
同理,可以使用@RequiresRoles("admin")注解來(lái)驗(yàn)證角色(身份)
在前端頁(yè)面使用Shiro標(biāo)簽時(shí)也會(huì)觸發(fā)權(quán)限控制,請(qǐng)看:http://www.itdecent.cn/p/6786ddf54582
其他兩種驗(yàn)證和授權(quán)請(qǐng)看:https://juejin.im/entry/5ad95ef26fb9a07a9f01185a
也可直接編代碼測(cè)試:
Boolean isPermitted = SecurityUtils.getSubject().isPermitted("***");//是否有什么權(quán)限
Boolean hasRole = SecurityUtils.getSubject().hasRole("***");//是否有什么角色
登陸實(shí)現(xiàn)
前端登陸頁(yè)面:
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Login</title>
</head>
<body>
錯(cuò)誤信息:<h4 th:text="${msg}"></h4>
<form action="" method="post">
<p>賬號(hào):<input type="text" name="username" value="admin"/></p>
<p>密碼:<input type="text" name="password" value="123456"/></p>
<p><input type="submit" value="登錄"/></p>
</form>
</body>
</html>
登陸接口:
@Controller
public class LoginController {
@RequestMapping("/login")
public String toLogin(HttpServletRequest request, Map<String, Object> map) {
System.out.println("HomeController.login()");
// 登錄失敗從request中獲取shiro處理的異常信息。
// shiroLoginFailure:就是shiro異常類的全類名.
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);
return "login";
}
@RequestMapping({"/","/index"})
public String index(){
return "index";
}
}
你可能發(fā)現(xiàn)了,登陸沒(méi)有用文章開(kāi)頭的Subject執(zhí)行登陸動(dòng)作,而是直接使用action=""的表單登錄。
為什么action=""呢,這是因?yàn)樵O(shè)置了"/**", "authc",當(dāng)用戶沒(méi)有登陸時(shí),所有url(/**)都會(huì)被重定向到/login,而action=""或者"/login"將不被攔截,doGetAuthenticationInfo()驗(yàn)證表單中的賬號(hào)密碼。
使用Subject執(zhí)行登陸動(dòng)作
那如何使用Subject執(zhí)行登陸動(dòng)作呢,需要使用user過(guò)濾器。當(dāng)沒(méi)有登錄時(shí),user跟authc一樣,會(huì)攔截所有的url。
//user:需要已登錄或“記住我”的用戶才能訪問(wèn);
filterChainDefinitionMap.put("/**", "user");
當(dāng)使用Post請(qǐng)求的/login登陸時(shí),將使用Subject執(zhí)行登陸動(dòng)作,doGetAuthenticationInfo()方法驗(yàn)證
<form action="/login" method="post">
<p>賬號(hào):<input type="text" name="username" value="admin"/></p>
<p>密碼:<input type="text" name="password" value="123456"/></p>
<p><input type="submit" value="登錄"/></p>
</form>
@RequestMapping(value = "/login", method = RequestMethod.POST)
public String login(@RequestParam(value = "username") String userName,
@RequestParam(value = "password") String password) {
UsernamePasswordToken token = new UsernamePasswordToken(userName, password);
Subject subject = SecurityUtils.getSubject();
try {
subject.login(token);
} catch (AuthenticationException e) {
e.printStackTrace();
}
return "index";
}
項(xiàng)目源碼
其余未貼出代碼請(qǐng)查看項(xiàng)目源碼:https://github.com/DeppWang/SpringBoot-Demo
總結(jié)
通過(guò)ShiroFilter配置的過(guò)濾器,Shiro攔截所有未過(guò)濾的url。如果未登陸,跳轉(zhuǎn)到登陸頁(yè)(loginUrl)。直接使用表單,或者使用subject.login()登陸時(shí)。在doGetAuthenticationInfo()中驗(yàn)證。驗(yàn)證通過(guò),跳轉(zhuǎn)到成功頁(yè)(successUrl)。
當(dāng)訪問(wèn)需要訪問(wèn)權(quán)限的url時(shí),從數(shù)據(jù)庫(kù)查詢當(dāng)前用戶的權(quán)限,最后交給Shiro框架去驗(yàn)證。在doGetAuthorizationInfo()中實(shí)現(xiàn)。
參考資料
springboot整合shiro-登錄認(rèn)證和權(quán)限管理:http://www.ityouknow.com/springboot/2017/06/26/springboot-shiro.html
30分鐘學(xué)會(huì)如何使用Shiro:http://www.cnblogs.com/learnhow/p/5694876.html