Shiro
1. 什么是权限管理
一般来说,一个系统对于不同的用户,提供的不同的功能,也就是权限不同。所以对于多角色的用户,一般需要进行权限管理,实现对用户访问系统的控制,按照安全规则或者安全策略控制用户只能访问自己被授权的资源。
权限管理包括对用户身份的 认证 和 授权 两部分。对于需要访问控制的资源,用户首先要经过身份认证,认证通过后,根据授予的权限,访问相应的资源。
认证
判断一个用户是否为合法用户。(用户登录校验、session验证、token验证都是常见的方式)
授权
也就是访问控制,为用户授予相应的权限,必须拥有一定的权限才能访问系统资源。(比如普通管理员,无权访问比较隐私的系统资源,只有高级管理员才可以等等)
2. Shiro简介
Apache Shiro™ is a powerful and easy-to-use Java security framework that performs authentication, authorization, cryptography, and session management. With Shiro’s easy-to-understand API, you can quickly and easily secure any application – from the smallest mobile applications to the largest web and enterprise applications.
Shiro是功能强大且易于使用的Java安全框架,可以进行身份验证、授权、加密和会话管理。使用Shiro提供的容易理解的API,可以快速轻松的保护任何应用程序——从最小的移动应用程序到最大的web和企业应用程序。
Shiro是Apache下的Simple Java Security。它将软件系统的安全认证相关的功能抽取出来,实现身份验证、授权、加密、会话管理等功能,组成了一个通用的安全认证框架。
3. Shiro的核心架构

3.1 Subject
主体,外部应用于Subject交互,Subject记录了当前操作的用户。将用户的概念理解为当前操作的主体,可能是一个通过浏览器请求的用户,也可以是一个运行的程序。
Subject在Shiro中是一个接口,定义了很多认证授权相关的方法,外部程序通过Subject,在Security Manager中进行认证授权。
3.2 Security Manager
安全管理器,对全部的Subject进行管理,是Shiro的 核心。其中包含了多个组件,分别对应不同的功能:
通过
Authenticator进行认证通过
Authorizer进行授权通过
Session Manager进行会话管理Security Manager是一个接口,继承了Authenticator、Authorizer、SessionManager这三个接口
3.3 Authenticator
认证器,是一个接口,提供对用户身份进行认证的功能。Shiro提供 ModularRealmAuthenticator 这个实现类,基本上可以满足大多数的认证需求,也可以自定义认证器。
3.4 Authorizer
授权器,用户通过认证器认证后,访问功能时,需要通过授权器判断用户是否拥有权限。
3.5 Realm
领域,相当于Data Source数据源,Security Manager进行安全认证和权限判断时,需要通过Realm连接到数据源(数据库或者配置文件),获取用户相应的身份信息和权限数据。
不要把Realm理解成只是从数据源取数据,在Realm中还有认证授权校验的相关代码
3.6 Session Manager
会话管理器,不依赖于web容器的Session,所以Shiro可以在非web应用上使用,也可以将分布式应用的会话集中在一点管理(实现单点登录)。
3.7 Session DAO
会话Dao,对session操作的一套接口,比如将session存入到数据库。
3.8 Cache Manager
缓存管理,将用户权限数据存储在缓存中,用户的权限只需要在第一次的时候通过Realm从数据源中获取,获取后会将其缓存在Cache Manager中,之后直接在这里获取即可,提高性能。
3.9 Cryptography
密码管理,提供了一套加密/解密的组件,方便开发。
4. Shiro中的认证
4.1 Shiro认证的关键对象
Subject:主体访问系统的用户、程序,都是主体,需要进行认证
Principal:身份信息主体进行身份认证的标识,必须具有
唯一性。一个主体可以有多个身份,但是必须要有一个主身份,用于认证。(相当于用户名)Credential:凭证信息只有主体自己知道的安全信息,比如密码、证书等。(相当于口令)
4.2 认证流程
首先将主体传入的身份信息+凭证信息转换成相应的 token → 安全管理器判断此token是否合法,只有合法,才可以进入系统。
而且只要认证通过,访问其他受限资源(需要先认证的资源),就不需要再进行认证了。(也就是说认证通过后,就不会再进行认证了。Shiro的任务就只剩下授权了)退出了登录,才需要重新认证。(subject.logout())
4.3 实操
引入依赖
<dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-core</artifactId> <version>1.5.3</version> </dependency>加入配置文件
Shiro的配置文件是.ini,位置的话放在resources下即可,命名没有特别要求。- 而且这个配置文件,只是为了在初学阶段,为了减少连接数据库的麻烦配置,
可以直接在这个配置文件中写死指定,方便熟悉Shiro的特性和功能。随着学习的深入,信息则都是从数据库中获取的(一般的认证、权限信息需要从数据库中获取),这个配置文件就不必要了。 - 或者在单机应用,不访问数据库的情况下使用配置文件来配置信息。
[users] # 固定写法,表明下面数据为认证信息,可配置多条
# 配置认证信息:用户名=密码
klane=123456
白鱼=lhy
- 认证的代码
- 注意不要导错包,都是Shiro依赖内的
- 认证成功时,不会有任何消息。认证失败时:
- 如果是
用户名不存在:抛出UnknownAccountException异常 - 如果是
密码错误:抛出IncorrectCredentialsException异常
- 如果是
public static void main(String[] args) {
//1.创建安全管理器对象(SecurityManager是接口,DefaultSecurityManager是实现类)
DefaultSecurityManager securityManager = new DefaultSecurityManager();
//2.给安全管理器对象设置Realm(读取放在resources的配置文件)
securityManager.setRealm(new IniRealm("classpath:shiro.ini"));
//3.使用全局安全工具类SecurityUtils,进行相应的认证和授权操作
//给SecurityUtils设置安全管理器
SecurityUtils.setSecurityManager(securityManager);
//4.拿到Subject对象
Subject subject = SecurityUtils.getSubject();
//5.创建令牌(Subject认证时需要通过 “身份信息” 和 “凭证信息” 生成令牌)
UsernamePasswordToken token = new UsernamePasswordToken("白鱼","lhy");
//6.通过Subject对象进行用户认证(认证通过,没有任何返回消息;认证失败,抛出异常)
try {
System.out.println("认证状态:" + subject.isAuthenticated()); //false
subject.login(token); //认证
System.out.println("认证状态:" + subject.isAuthenticated()); //true
} catch (UnknownAccountException e) {
System.out.println("用户不存在");
e.printStackTrace();
} catch (IncorrectCredentialsException e){
System.out.println("密码不正确");
e.printStackTrace();
}
}
4.4 认证的源码浅析
最终执行身份信息校验的是在
SimpleAccountRealm中的doGetAuthenticationInfo方法中进行校验最终执行凭证信息校验的是在
AuthenticatingRealm中的assertCredentialsMath方法中自动校验protected void assertCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) throws AuthenticationException { CredentialsMatcher cm = this.getCredentialsMatcher(); if (cm != null) { if (!cm.doCredentialsMatch(token, info)) { // !!! String msg = "Submitted credentials for token [" + token + "] did not match the expected credentials."; throw new IncorrectCredentialsException(msg); } } else { throw new AuthenticationException("A CredentialsMatcher must be configured in order to verify credentials during authentication. If you do not wish for credentials to be examined, you can configure an " + AllowAllCredentialsMatcher.class.getName() + " instance."); } }这个方法内调用了
CredentialsMatcher接口的doCredentialsMatch,不同的实现类对此方法有不同的实现,但都是获取到token的凭证,再获取到数据源中的凭证,两者进行equals判断而已。//SimpleCredentialsMatcher类中重写的方式 public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) { Object tokenCredentials = this.getCredentials(token); Object accountCredentials = this.getCredentials(info); return this.equals(tokenCredentials, accountCredentials); }
常见的Realm关系

AuthenticatingRealm认证Realm(抽象类),提供了doGetAuthenticationInfo抽象方法,用于认证AuthorizingRealm授权Realm(抽象类),提供了doGetAuthorizationInfo抽象方法, 用于授权
而AuthorizingRealm 又继承了AuthenticatingRealm ,所以SimpleAccountRealm只要继承了AuthorizingRealm 抽象类,就同时拥有上面授权和认证所需要的接口,可以进行重写。
也就是说,如果我们要自定义Realm,就需要去继承AuthorizingRealm这个抽象类,然后重写doGetAuthorizationInfo(用于授权)和doGetAuthenticationInfo(用于认证)方法即可。
4.5 自定义Realm
上面说到的,自定义Realm需要继承AuthorizingRealm,然后重写两个方法
//自定义Realm,将认证、授权的数据源转为从数据库中获取
public class MyRealm extends AuthorizingRealm {
//授权
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
return null;
}
//认证
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
//获取到token中拿到的身份凭证
String principal = (String) token.getPrincipal();
System.out.println(principal);
//之后可以根据身份信息使用jdbc mybatis查询相关数据库即可
//参数1:身份信息(用户名) 参数2:数据库中正确的凭证信息(密码) 参数3:提供当前Realm的名字
SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo("klane","123",this.getName());
return simpleAuthenticationInfo;
}
}
至于使用,只是在安全管理器中设置Realm的时候,new我们自定义的Realm即可。
//创建安全管理器
DefaultSecurityManager securityManager = new DefaultSecurityManager();
//设置自定义的Realm
securityManager.setRealm(new MyRealm()); //!!
//将安全管理器设置到安全工具类中
SecurityUtils.setSecurityManager(securityManager);
//通过安全工具类获取到Subject
Subject subject = SecurityUtils.getSubject();
//创建令牌
UsernamePasswordToken token = new UsernamePasswordToken("klane", "123456");
//身份验证
try {
subject.login(token);
} catch (UnknownAccountException e) {
System.out.println("用户不存在");
e.printStackTrace();
} catch (IncorrectCredentialsException e){
System.out.println("密码不正确");
e.printStackTrace();
}
4.6 凭证加密处理
这里使用MD5+Salt的方式进行实现。将盐和散列后的值分别存入在数据库中,Realm从数据库中取出盐和加密后的值,与token中获取到的密码以及前面的盐去拼接并散列后,进行比较。
MD5
作用:一般用来加密 或 签名(校验和)
特点:不可逆,一致性
结果:16进制 32位的字符串
应用:
加密使用
校验(比如判断两个文件是否内容相同,就可以通过比较两者的MD5值)
MD5要破解,只能通过穷举进行破解,所以如果数据库信息被盗窃了,虽然有在线网站之类的可以进行穷举破解出原文,但是只要密码过于复杂,穷举的难度也就加大了。
所以,为了避免有的用户真的使用了简单的密码,我们可以通过加盐的方式来复杂化密码(你不体面,我们帮你体面)。这个盐可以是随机的,不同用户有不同的盐,保存在数据库中。
即使这个盐和MD5后的密文都被盗窃了,但是由于我们通过盐复杂化了,要破解复杂化的MD5密文,也还是很难的,而且还需要清楚盐拼接在哪里(前面?中间?后面?),又加上了一层保障。
当然了,若是真的被穷举到了,知道了明文,又知道了盐,人脑判断一下拼接规则,那么这个用户也就木大啦,黑客就可以使用了这个拼接规则去写自动处理的程序了)
加密的代码实现(用在注册的时候)
public static void main(String[] args) { //创建普通的一个md5算法。 Md5Hash md5Hash = new Md5Hash("123"); //注意:不能通过setBytes的方式传入要加密的数据,这样子并不会进行MD5加密 //md5Hash.setBytes("12345".getBytes()); //创建加盐的md5。 参数1:要加密的数据 参数2:盐 //默认是将盐拼接到数据的面,再去进行MD5散列 Md5Hash md5Hash = new Md5Hash("123", "1*sp23v"); //md5 + salt + 散列次数(默认是一次) Md5Hash md5Hash = new Md5Hash("123", "1*sp23v", 1024); System.out.println(md5Hash.toHex()); //转成16进制(32个字符) }认证的代码实现(用在登录的时候)
注意:前面我们使用的时候是没有用到MD5的,所以对于凭证信息的校验,使用的是
SimpleCredentialsMatcher的doCredentialsMatch方法,这个方法是无法对凭证信息进行相应的MD5运算,再去和数据库中保存的加密后的凭证去比较,所以我们需要为Realm设置一个合适的凭证匹配器(HashedCredentialsMatcher)。//创建安全管理器 DefaultSecurityManager securityManager = new DefaultSecurityManager(); //创建自定义的Realm MyRealm myRealm = new MyRealm(); //创建可以处理MD5的凭证匹配器 (在这一部分处理,其他的一样不变) HashedCredentialsMatcher credentialsMatcher = new HashedCredentialsMatcher(); credentialsMatcher.setHashAlgorithmName("md5"); //指定散列函数名 credentialsMatcher.setHashIterations(1024);//指定散列次数 myRealm.setCredentialsMatcher(credentialsMatcher); //这是到自定义Realm中 securityManager.setRealm(myRealm); //将安全管理器设置到安全工具类中 SecurityUtils.setSecurityManager(securityManager); //通过安全工具类获取到Subject Subject subject = SecurityUtils.getSubject(); //创建令牌 UsernamePasswordToken token = new UsernamePasswordToken("klane", "123456"); //身份验证 try { subject.login(token); System.out.println(subject.isAuthenticated()); } catch (UnknownAccountException e) { System.out.println("用户不存在"); e.printStackTrace(); } catch (IncorrectCredentialsException e){ System.out.println("密码不正确"); e.printStackTrace(); }同时对于自定义的Realm,也有需要进行改变的部分
public class MyRealm extends AuthorizingRealm { //授权 …… //认证 @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { //获取到token中拿到的身份凭证 String principal = (String) token.getPrincipal(); System.out.println(principal); //之后可以根据身份信息使用jdbc mybatis查询相关数据库即可 //参数1:身份信息(用户名) 参数2:数据库中正确的凭证信息(密码 + md5 + salt) //参数3:(注册时生成的)随机盐 参数4:提供当前Realm的名字 SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo("klane", "aa901020133e0926060b7de4c1d0ccf0", ByteSource.Util.bytes("1*sp23v"), this.getName()); return simpleAuthenticationInfo; } }
5. Shiro的授权
5.1 Shiro授权的关键对象
Subject(主体)就是谁要来访问。(认证通过是前提)
Resource(资源)如系统菜单、按钮、页面、请求路径、方法、商品信息等等。资源包括
资源类型和资源实例。商品信息为资源类型,具体的某一类商品、某一个商品是资源实例。
Permission(权限)规定了主体对资源的操作许可,权限离开资源没有意义(所以权限需要和资源进行绑定,当然了,权限也是需要和主体进行绑定的)。
权限 = 资源 + 操作(CURD)
授权就是:定义 “主体” 对哪些 “资源” 具有某些 “操作/权限”。
5.2 授权方式
基于
角色的访问控制(Role-Based Access Control)if(subject.hasRole("admin")){ //具有admin角色的所有操作权限 }基于
资源的访问控制(Resource-Based Access Control)if(subject.isPermitted("goods:find:*")){ //对所有的商品具有查询权限 } if(subject.isPermitted("goods:*:01")){ //对编号为01的商品,具有任意权限(CURD) }
5.3 权限字符串
上面基于资源的访问控制,使用的就是权限字符串进行判断。
权限字符串的规则是:资源标识符 : 操作 : 资源实例表示符。例子:
- goods : find : *(可以省略成goods : find) 对所有的商品具有查询权限
- goods : * : 01 对编号为01的商品,具有任意权限(CURD)
- * : * : * 对所有资源,所有实例,拥有所有的操作(超级管理员)
5.4 代码中实现授权的方式
5.4.1 编程式
if(subject.hasRole("admin")){
//有权限
} else{
//无权限
}
5.4.2 注解式
@RequiresRoles("admin")
public void hello(){
//有权限
}
5.4.3 标签式(少)
<!--在JSP/GSP页面中使用标签-->
<shiro:hasRole name="admin"> 有权限的操作 </shiro:hasRole>
<!--thymeleaf使用shiro需要额外集成,JSP可以直接使用-->
5.5 实操
5.5.1 基于角色的访问控制
- 在自定义Realm中,重写
doGetAuthorizationInfo方法
//授权
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
//获取主身份信息(其实就是用户名)
String primaryPrincipal = (String)principalCollection.getPrimaryPrincipal();
System.out.println("主身份:" + primaryPrincipal);
//根据这个用户名,可以去查询数据库,获取到当前用户的角色信息以及权限信息
……
//返回目标对象
SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
//设置此用户拥有的角色(从数据库中查询出来的)
simpleAuthorizationInfo.addRole("admin");
simpleAuthorizationInfo.addRole("user");
return simpleAuthorizationInfo;
}
- 在认证通过的前提下,判断是否拥有角色
//基于单角色访问控制
if(subject.hasRole("admin")){
//操作
}
//如果需要同时拥有多个角色
if(subject.hasAllRoles(Arrays.asList("admin", "user"))){
//操作
}
//判断是否拥有指定角色
boolean[] booleans = subject.hasRoles(Arrays.asList("admin", "user", "super"));
for(boolean b: booleans){
System.out.println(b);
}
5.5.2 基于权限字符串的访问控制
- 在自定义Realm中,重写
doGetAuthorizationInfo方法
//核心代码,设置权限信息
simpleAuthorizationInfo.addStringPermission("user:*:*");
- 在认证通过的前提下,判断是否拥有权限
/**核心代码**/
//判断是否具有权限(true)
boolean b = subject.isPermitted("user:update:01")
//同时具有哪些权限
boolean b = subject.isPermittedAll("", "", "", ……);
//多个权限判断
boolean[] booleans = subject.isPermitted("", "", "", ……);
6. Shiro整合SpringBoot项目实战
6.1 引入依赖
<!--Shiro-->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring-boot-starter</artifactId>
<version>1.5.3</version>
</dependency>
<!--thymeleaf-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<!--数据库相关-->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.3</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.39</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.16</version>
</dependency>
6.2 配置Shiro
- 创建
配置类
@Configuration
public class ShiroConfig {
//创建ShiroFilter(用于拦截所有请求,对受限资源进行Shiro的认证和授权判断)
@Bean
public ShiroFilterFactoryBean getShiroFilterFactoryBean(DefaultWebSecurityManager defaultWebSecurityManager){
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
//设置安全管理器
shiroFilterFactoryBean.setSecurityManager(defaultWebSecurityManager);
//配置系统的受限资源
Map<String, String> map = new HashMap<>();
// map.put("/index","authc"); //key:资源路径 value:标识为受限资源(需要认证和授权)
//如果是/**,表示所有资源都是受限资源
map.put("/**", "authc");
map.put("/login", "anon"); //登录路径、注册页面、注册路径都需要放行(公共资源,登录页面已经是了)
map.put("/registerPage", "anon");
map.put("/register", "anon");
shiroFilterFactoryBean.setFilterChainDefinitionMap(map);
//指定认证不通过时的跳转路径(默认是/login.jsp,从这里可以看出shiro和jsp的好基友关系)
shiroFilterFactoryBean.setLoginUrl("/loginPage");
return shiroFilterFactoryBean;
}
//创建安全管理器(对于web应用,不能使用DefaultSecurityManager,不具备web功能)
//而且会自动设置到SecurityUtils中设置这个安全管理器,不需要手动设置
@Bean
public DefaultWebSecurityManager getDefaultWebSecurityManager(Realm realm){
DefaultWebSecurityManager defaultWebSecurityManager = new DefaultWebSecurityManager();
//给安全管理器设置realm
defaultWebSecurityManager.setRealm(realm);
return defaultWebSecurityManager;
}
//创建自定义Realm
@Bean
public Realm getRealm(){
CustomerRealm realm = new CustomerRealm();
//修改凭证校验匹配器(处理加密)
HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
hashedCredentialsMatcher.setHashAlgorithmName("MD5");
hashedCredentialsMatcher.setHashIterations(1024);
realm.setCredentialsMatcher(hashedCredentialsMatcher);
return realm;
}
}
- Shiro提供的过滤器
上面配置类中,配置了ShiroFilter,通过map保存了受限资源。value为:authc,其实是Shiro提供了一种过滤器。Shiro还提供了其他的过滤器。
| 配置缩写 | 对应的过滤器 | 功能 |
|---|---|---|
| anon | AnnoymousFilter | 指定的URL无需经过认证,可以直接访问(即公共资源) |
| authc | FormAuthenticationFilter | 指定的URL需要form表单登录。如果登录不成功毁跳转到loginUrl配置的路径。(即受限资源) |
| authcBasic | BasicHttpAuthenticationFilter | 需要Basic登录 |
| logout | LogoutFilter | 登出过滤器,配置指定的URL就可以实现退出功能 |
| noSessionCreation | NoSessionCreationFilter | 禁止创建会话 |
| perms | PermissionsAuthorizationFilter | 需要指定权限才能访问 |
| port | PortFilter | 需要指定端口才能访问 |
| rest | HttpMethodPermissionFilter | 将http请求方法转成相应的动词来构造一个权限字符串 |
| roles | FolesAuthorizationFilter | 需要指定角色才能访问 |
| ssl | SslFilter | 需要https请求才能访问 |
| user | UserFilter | 需要已登录或者“记住我”的用户才能访问 |
- 定义
自定义Realm
这个在下面登录认证的时候粘代码
6.3 数据库
数据库设计,对于权限部分,可以有多种设计方式:
1、
用户表+角色表+权限表→(用户拥有多个角色,角色拥有多个权限)2、用户表 + 角色表
3、用户表 + 权限表
上面表之间的关系,都是多对多的关系,所以还需要另外建立相应的**
关联表**
这里采取第一种方式,也是比较常见的方式。

- 用户表
CREATE TABLE `t_user` (
`id` int(6) NOT NULL AUTO_INCREMENT,
`username` varchar(20) DEFAULT NULL,
`password` varchar(40) DEFAULT NULL,
`salt` varchar(255) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8
- 角色表
CREATE TABLE `t_role` (
`id` int(6) NOT NULL AUTO_INCREMENT,
`rolename` varchar(60) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8
- 权限表
CREATE TABLE `t_pers` (
`id` int(6) NOT NULL AUTO_INCREMENT,
`persname` varchar(60) NOT NULL,
`url` varchar(255) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8
- 用户-角色关联表
CREATE TABLE `t_user_role` (
`id` int(6) NOT NULL AUTO_INCREMENT,
`userid` int(6) NOT NULL,
`roleid` int(6) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8
- 角色-权限关联表
CREATE TABLE `t_role_pers` (
`id` int(6) NOT NULL AUTO_INCREMENT,
`roleid` int(6) NOT NULL,
`persid` int(6) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8
6.4 注册时加密
- controller、mapper层的比较简单就不放代码了,核心就是service中对密码加密的处理
//service中的注册方法
public void register(User user) {
//对密码进行:MD5 + salt + 散列
String salt = SaltUtil.getSalt(8);
user.setSalt(salt); //salt也要保存
Md5Hash md5Hash = new Md5Hash(user.getPassword(), salt, 1024);
user.setPassword(md5Hash.toHex());
//保存到数据库中
userMapper.save(user);
}
public class SaltUtil {
//随机生成n位的盐
public static String getSalt(int n){
char[] chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqestuvwxyz0123456789!@#$%^&*".toCharArray();
StringBuilder sb = new StringBuilder();
for (int i = 0; i < n; i++) {
char c = chars[new Random().nextInt(chars.length)];
sb.append(c);
}
return sb.toString();
}
}
6.5 登录时 认证
- 自定义的Realm要重写
public class CustomerRealm extends AuthorizingRealm {
// @Autowired
// private UserService userService;
//授权
……
//认证
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
//在工厂中获取userMapper对象(或者使用自动注入),说是使用工厂的这种方式比较好??
UserService userService = (UserService) ApplicationContextUtil.getBean("userServiceImpl");
String principal = (String) authenticationToken.getPrincipal();
//根据用户名,获取到相应的User对象
User user = userService.findByUserName(principal);
if(user != null){
return new SimpleAuthenticationInfo(user.getUsername(),
user.getPassword(),
ByteSource.Util.bytes(user.getSalt()),
this.getName());
}
return null;
}
}
//xxxAware接口,可以在提供需要重写的方法中,通过参数的方式获取到相应的xxx对象
@Component
public class ApplicationContextUtil implements ApplicationContextAware {
private static ApplicationContext context;
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.context = applicationContext;
}
//根据bean的名字获取工厂中的bean对象
public static Object getBean(String beanName){
return context.getBean(beanName);
}
}
6.6 进入后授权
- 重写Realm中的
doGetAuthorizationInfo授权方法
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
//获取主身份信息(用户名)
String primaryPrincipal = (String)principalCollection.getPrimaryPrincipal();
//通过主身份,查询数据库,获取对应的角色、权限信息
UserService userService = (UserService) ApplicationContextUtil.getBean("userServiceImpl");
List<Role> roles = userService.findRolesByUserName(primaryPrincipal);
if(!CollectionUtils.isEmpty(roles)){
SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
roles.forEach(role->simpleAuthorizationInfo.addRole(role.getRolename()));
return simpleAuthorizationInfo;
}
return null;
}
- controller中进行判断(编程式、注解式)
- 在前后端分离的项目中,标签式比较少用
@Controller
@RequestMapping("/order")
public class OrderController {
@RequestMapping("/save")
// @RequiresRoles("user") //注解的方式(以角色为例),注意:没权限会抛异常
// @RequiresPermissions("user:save:*")
public String saveOrder(){
//代码的方式(以角色为例)
Subject subject = SecurityUtils.getSubject();
if(subject.hasRole("admin")){
System.out.println("保存订单信息!");
}
else{
System.out.println("无权访问!");
}
return "redirect:/index";
}
}
这里是基于角色去简单判断授权的。至于权限表部分,其实差不多,也都是获取后添加到授权方法中返回的对象即可。
7. Shiro的缓存
给用户的授权数据加上缓存,避免每次都需要调用Realm的doGetAuthorizationInfo方法判断权限(每次都需要去查询数据源),减轻DB的访问压力,同时提高系统查询效率。
Shiro提供了Cache Manager接口,来实现缓存功能。
7.1 EhCache
Shiro默认是实现EhCache,实现 本地缓存。(服务器重启,缓存则清空)
- 引入EhCache依赖
<!--EhCache缓存-->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-ehcache</artifactId>
<version>1.5.3</version>
</dependency>
- 在自定义Realm中,开启缓存
//开启缓存管理
realm.setCacheManager(new EhCacheManager()); //添加缓存管理器
realm.setCachingEnabled(true); //开启全局缓存
realm.setAuthenticationCachingEnabled(true); //开启认证缓存
realm.setAuthenticationCacheName("authenticationCache"); //设置认证缓存名
realm.setAuthorizationCachingEnabled(true); //开启授权缓存
realm.setAuthorizationCacheName("authorizationCache"); //设置授权缓存名
- 可以开启debug模式,看是否有SQL语句的日志输出。(只有第一次访问才有)
7.2 Redis
结合Redis,实现 分布式缓存。
引入springboot整合redis的依赖
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency>配置redis信息
spring.redis.port=6379 spring.redis.host=127.0.0.1 spring.redis.database=0自定义Shiro缓存管理器(结合Redis)
public class RedisCacheManager implements CacheManager {
//参数s:认证或者授权的名称
@Override
public <K, V> Cache<K, V> getCache(String s) throws CacheException {
return new RedisCache<K, V>(s);
}
}
- 自定义RedisCache
public class RedisCache<K, V> implements Cache<K, V> {
private String cacheName;
public RedisCache() {
}
public RedisCache(String cacheName) {
this.cacheName = cacheName;
}
//通过redis的hash数据类型,利用到了cacheName,方便之后管理
private RedisTemplate getRedisTemplate(){
RedisTemplate redisTemplate = (RedisTemplate) ApplicationContextUtil.getBean("redisTemplate");
redisTemplate.setKeySerializer(new StringRedisSerializer()); // 设置key的序列化方式
redisTemplate.setHashKeySerializer(new StringRedisSerializer()); //hash中的小key
return redisTemplate;
}
@Override
public V get(K k) throws CacheException {
return (V) getRedisTemplate().opsForHash().get(this.cacheName, k.toString());
}
@Override
public V put(K k, V v) throws CacheException {
getRedisTemplate().opsForHash().put(this.cacheName, k.toString(), v);
return null; //put操作返回值意义不大
}
//用户退出时调用(注意:如果是最后一个用户,进行退出后,连同map都会被清除)
@Override
public V remove(K k) throws CacheException {
return (V) getRedisTemplate().opsForHash().delete(this.cacheName, k.toString());
}
@Override
public void clear() throws CacheException {
getRedisTemplate().delete(this.cacheName);
}
@Override
public int size() {
return getRedisTemplate().opsForHash().size(this.cacheName).intValue();
}
@Override
public Set<K> keys() {
return getRedisTemplate().opsForHash().keys(this.cacheName);
}
@Override
public Collection<V> values() {
return getRedisTemplate().opsForHash().values(this.cacheName);
}
}
- 在自定义Realm中,设置成我们自定义的这个
RedisCacheManager即可。
//开启缓存管理
realm.setCacheManager(new RedisCacheManager()); //添加缓存管理器
realm.setCachingEnabled(true); //开启全局缓存
realm.setAuthenticationCachingEnabled(true); //开启认证缓存
realm.setAuthenticationCacheName("authenticationCache"); //设置认证缓存名
realm.setAuthorizationCachingEnabled(true); //开启授权缓存
realm.setAuthorizationCacheName("authorizationCache"); //设置授权缓存名
注意,由于认证和授权都需要将信息缓存到Redis中,而在认证的时候添加了盐,
ByteSource.Util.bytes(user.getSalt()。但是ByteSource没有办法序列化,所以需要自定义一个ByteSource,既要拥有ByteSource的功能,又要实现序列化接口//使用的代码是SimpleByteSource的源码, //只不过在其基础上,添加了无参构造,并把bytes全局变量去掉final。这是为了可以成功序列化 public class MyByteSource implements Serializable, ByteSource { private byte[] bytes; private String cachedHex; private String cachedBase64; //无参构造器 public MyByteSource(){ } public MyByteSource(byte[] bytes) { this.bytes = bytes; } public MyByteSource(char[] chars) { this.bytes = CodecSupport.toBytes(chars); } public MyByteSource(String string) { this.bytes = CodecSupport.toBytes(string); } public MyByteSource(ByteSource source) { this.bytes = source.getBytes(); } public MyByteSource(File file) { this.bytes = (new MyByteSource.BytesHelper()).getBytes(file); } public MyByteSource(InputStream stream) { this.bytes = (new MyByteSource.BytesHelper()).getBytes(stream); } public static boolean isCompatible(Object o) { return o instanceof byte[] || o instanceof char[] || o instanceof String || o instanceof ByteSource || o instanceof File || o instanceof InputStream; } public byte[] getBytes() { return this.bytes; } public boolean isEmpty() { return this.bytes == null || this.bytes.length == 0; } public String toHex() { if (this.cachedHex == null) { this.cachedHex = Hex.encodeToString(this.getBytes()); } return this.cachedHex; } public String toBase64() { if (this.cachedBase64 == null) { this.cachedBase64 = Base64.encodeToString(this.getBytes()); } return this.cachedBase64; } public String toString() { return this.toBase64(); } public int hashCode() { return this.bytes != null && this.bytes.length != 0 ? Arrays.hashCode(this.bytes) : 0; } public boolean equals(Object o) { if (o == this) { return true; } else if (o instanceof ByteSource) { ByteSource bs = (ByteSource)o; return Arrays.equals(this.getBytes(), bs.getBytes()); } else { return false; } } private static final class BytesHelper extends CodecSupport { private BytesHelper() { } public byte[] getBytes(File file) { return this.toBytes(file); } public byte[] getBytes(InputStream stream) { return this.toBytes(stream); } } }然后我们自定义的Realm的验证方法,盐的传入,换成我们自定义的ByteSource
new SimpleAuthenticationInfo(user.getUsername(), user.getPassword(), new MyByteSource(user.getSalt()), this.getName());
8. Shiro与Thymeleaf的结合(标签式)
8.1 引入扩展依赖
<!--Thymeleaf&Shiro-->
<dependency>
<groupId>com.github.theborakompanioni</groupId>
<artifactId>thymeleaf-extras-shiro</artifactId>
<version>2.0.0</version>
</dependency>
8.2 在html页面中引入命名空间
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns="http://www.thymeleaf.org"
xmlns="http://www.pollix.at/thymeleaf/shiro">
8.3 加入Shiro的方言配置,使标签生效
@Configuration
public class ShiroConfig{
@Bean(name="shiroDialect")
public ShiroDialect shiroDialect(){
return new ShiroDialect();
}
}
至于标签的使用,这里就不列举了。
-----------END(待续)-------------