Shiro
Apache Shiro 是一个强大易用的 Java 安全框架,提供了认证、授权、加密和会话管理等功能,对于任何一个应用程序,Shiro 都可以提供全面的安全管理服务。并且相对于其他安全框架,Shiro 要简单的多。
第一部分 Hello Shiro
1. 导入依赖
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
<version>1.5.3</version>
</dependency>
2. 配置shiro.ini
shiro.ini用于存放临时数据,降低学习者成本(也就是模拟数据库数据)
[users] // 注意格式 users表示用户数据列表
zhangsan=123
lisi=123456
3. 编写shiro代码认证
// 1.创建安全管理器
DefaultSecurityManager securityManager = new DefaultSecurityManager();
// 2设置凭证,也就是加载 shiro.ini配置文件
securityManager.setRealm(new IniRealm("classpath:shiro.ini"));
// 3.给安全工具设置安全管理器 安全工具为框架提供的工具帮助我们快速实现认证授权
SecurityUtils.setSecurityManager(securityManager);
// 4.通过安全工具获取主体(可以理解为登录用户的实体)
Subject subject = SecurityUtils.getSubject();
// 5.创建登录令牌信息
UsernamePasswordToken token = new UsernamePasswordToken("lisi","123456");
try{
// 6.登录验证
subject.login(token);
// 7.获取验证状态
System.out.println(subject.isAuthenticated());
}catch (Exception e){
e.printStackTrace();
}
第二部分 shiro核心类
1. SecurityManager
安全管理器,可以说是Shiro的心脏,所有的认证授权都会交给他,然后他再做处理
① 实现
DefaultSecurityManager securityManager = new DefaultSecurityManager();
一般情况下我们用这个实现类进行代码实现
② 设置安全数据
也就是我们数据库中存放的数据
数据写死,所以这里直接设置一个IniRealm
securityManager.setRealm(new IniRealm("classpath:shiro.ini"));
数据库数据,不管怎样我们最后都要替换为数据库数据
securityManager.setRealm(new custom());
custom 为自己实现的对象,会在后续接触到,在这里面我们设置数据库的数据
2. SecurityUtils
框架提供的工具类,帮助我们快速实现登录认证
方法
① setSecurityManager
将我们定义好的SecurityManager交给他,毕竟核心是SecurityManager。
② 获取Subject
帮助我们获取一个基本是啥都没有的Subject
3. UsernamePasswordToken
创建一个登录令牌既token
UsernamePasswordToken token = new UsernamePasswordToken("lisi","123456");
4. Subject
认证主体,可以理解为登陆的用户,因为后续的所有于该用户相关的数据都会放在这里面
① 实现
通过框架提供的SecurityUtils直接获取
Subject subject = SecurityUtils.getSubject();
② 登录
算是核心方法了,完成用户认证
try{
// 登录验证
subject.login(token);
// 获取验证状态
System.out.println(subject.isAuthenticated());
}catch (UnknownAccountException e){
e.printStackTrace();
System.out.println("用户账号错误");
}catch (IncorrectCredentialsException e){
System.out.println("用户认证凭证错误");
e.printStackTrace();
}
认证之后的信息会存储在Subject中
如果认证失败会抛出错误异常,成功则不会抛出异常
第三部分 自定义身份认证
为什么要自定义?
从上述案例我们发现,用户数据是写死的,密码判断我们也没有参与
- 账号判断,我们默认使用的是IniRealm,是Shiro已经定义好的匹配方法,其中的数据也有我们写死,但实际上我们要是用数据数据进行判断,所以我们要自定义。
- 凭证判断(通常是指密码),默认Shiro通过断言方式比较,但是我们通常需要对密码需要加密,所以要自定义比较方法
1. 密码加密
加密方式通常有三种MD5、随机盐、hash三列,通常我们混合使用
① MD5
- 加密不可逆,加密回去就推不回来了
- MD5加密是将我们的明文加工成一串32位的十六进制数
② 随机盐
就是在用户密码MD5之前随机添加几位数,防止用户密码太简单,被穷举出来
③ hash散列
在随机盐以及MD5基础上在对密码进行散列,进一步加密密码
使用
shiro给我们提供了Md5Hash
// 直接使用方法
Md5Hash md5Hash = new Md5Hash("123");
System.out.println(md5Hash.toHex());
Md5Hash md5Hash2 = new Md5Hash("123","kjaspof");
System.out.println(md5Hash2.toHex());
Md5Hash md5Hash3 = new Md5Hash("123","kjaspof",1024);
System.out.println(md5Hash3.toHex());
// 参数一是 用户明文密码
// 参数二是 随机盐字符串
// 参数三是 hash散列次数 一般是1024/2048
注:必须要使用Md5Hash的构造方法传值
2. 自定义身份认证
查看源码得知,账号匹配在AuthorizingRealm(类)一脉中的 doGetAuthenticationInfo 中进行
如果我们在编码时Realm设定为 IniRealm,那么账号匹配就是在IniRealm的doGetAuthenticationInfo方法中进行
所以要自定义账号匹配也就是认证,那就需要自定义个Realm继承AuthorizingRealm实现其中的 doGetAuthenticationInfo 方法
① 密码未加密
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
public class CustomerRealm extends AuthorizingRealm {
// 授权方法
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
return null;
}
// 认证方法
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
// 参数就是我们subject.login(token)时传递的UsernamePasswordToken
// 获取用户登录身份,一般是账号
Object principal = authenticationToken.getPrincipal();
// 获取用户登录凭证,一般是密码
Object credentials = authenticationToken.getCredentials();
// TODO:获取数据库数据 进行一系列操作
// 当传入数据Shiro会进行数据匹配与断言,当账号不正确时抛出UnknownAccountException 密码不正确时抛出IncorrectCredentialsException
SimpleAuthenticationInfo authenticationInfo = null;
authenticationInfo = new SimpleAuthenticationInfo("数据库用户名", "数据库密码", this.getName());
return authenticationInfo;
}
}
① 密码加密
密码进行加密,那第一部我们要告诉Shiro我们用的什么加密
查看源码得知,密码断言是在Realm的 CredentialsMatcher 属性中进行,那我们就要自定义一个CredentialsMatcher
// 自定义一个CredentialsMatcher 设置加密方法为md5
HashedCredentialsMatcher credentialsMatcher = new HashedCredentialsMatcher();
credentialsMatcher.setHashAlgorithmName("md5");
// 如果还使用了散列,那就要设置散列次数
credentialsMatcher.setHashIterations(1024);
// 将其设置到我们的Realm中
CustomerRealm customerRealm = new CustomerRealm();
customerRealm.setCredentialsMatcher(credentialsMatcher);
这样Shrio在进行密码断言的时候就会先对密码进行MD5加密或散列,再进行断言。并且兼容随机盐(需要其他操作)
如果我们还用了随机盐,就需要对我们自定义的Realm方法进行改变
package org.example;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.util.ByteSource;
public class CustomerRealm extends AuthorizingRealm {
// 授权方法
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
return null;
}
// 认证
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
// 参数就是我们subject.login(token)时传递的UsernamePasswordToken
// 获取用户登录身份,一般是账号
Object principal = authenticationToken.getPrincipal();
// 获取用户登录凭证,一般是密码
Object credentials = authenticationToken.getCredentials();
// TODO:获取数据库数据 进行一系列操作
// 当传入数据Shiro会进行数据匹配与断言,当账号不正确时抛出UnknownAccountException 密码不正确时抛出IncorrectCredentialsException
SimpleAuthenticationInfo authenticationInfo = null;
authenticationInfo = new SimpleAuthenticationInfo("数据库用户名", "数据库密码", ByteSource.Util.bytes("随机盐字符串"), this.getName());
return authenticationInfo;
}
}
第四部分 自定义授权
授权在认证之后
也就是说subject通过认证之后开始对其拥有的权限进行操作
1. 授权(授予subject权限)
在自定义的Realm中的doGetAuthorizationInfo方法中进行
// 授权方法
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
// 用户凭证
String userName = (String) principalCollection.getPrimaryPrincipal();
// 授权对象
SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
// 1.身份授权
authorizationInfo.addRole("admin");
authorizationInfo.addRole("user");
// 2.资源权限授权
authorizationInfo.addStringPermission("user:*:01");
authorizationInfo.addStringPermission("product:create:*");
return authorizationInfo;
}
授权方式:
- 角色授权:admin,user等
- 资源访问授权 product:create:* 分为三级,从左到右一次类推,“*”代表全部
不同的授权方式,使用的方法也不尽相同
2. 权限认证
认证权限,判断该用户是否拥有某一个角色或者资源访问权限
方法类似身份认证,返回值为boolean
// 1.身份授权
// 单一身份
System.out.println(subject.hasRole("admin"));
// 多个身份
System.out.println(subject.hasAllRoles(Arrays.asList("admin","user")));
// 任一身份
boolean[] booleans = subject.hasRoles(Arrays.asList("admin", "user"));
for (boolean boo : booleans) {
System.out.println(boo);
}
// 2.资源授权
// 单一资源权限
System.out.println(subject.isPermitted("user:save:01"));
// 多个资源权限
System.out.println(subject.isPermittedAll("user:save:01","product:update:*"));
// 任一资源权限
boolean[] booleans = subject.isPermitted("user:save:01", "product:update:*");
for (boolean b : booleans) {
System.out.println(b);
}
第五部分 认证流程源码解析
1. 执行流程
流程前提:
// 默认的安全管理器
DefaultSecurityManager securityManager = new DefaultSecurityManager();
// 自定义的CustomeRealm
securityManager.setRealm(new CustomerRealm());
// 入口
subject.login(token);
认证流程追踪subject.login(token)方法
-
Subject调用自己的 login(AuthenticationToken token) 方法进行认证登录
-
login方法调用DefaultSecurityManager的login(Subject subject, AuthenticationToken token) 方法进行认证
-
login方法调用父类AuthenticatingSecurityManagerd的authenticate(AuthenticationToken token)方法进行认证
-
authenticate方法调用自身的realm认证管理器ModularRealmAuthenticator的authenticate(token)方法进行认证
-
authenticate:
- 断言是否存在Realm
- 获取所有的realm
- 根据realm个数使用不同的逻辑进行认证
- 如果realm为一个则调用自身单realm方法进行认证
- 如果realm为多个则调用,多realm逻辑进行认证,多realm下会使用到realm的认证策略。shiro提供三种认证策略
- AllSuccessfulStrategy,这个表示所有的 Realm 都认证成功才算认证成功
- AtLeastOneSuccessfulStrategy,这个表示只要有一个 Realm 认证成功就算认证成功,默认策略
- FirstSuccessfulStrategy,这个表示只要第一个 Realm 认证成功,就算认证成功
-
调用realm的doGetAuthenticationInfo(AuthenticationToken authenticationToken)方法进行认证以及后续处理
2. 多Realm配置
比如说:现在我们需要一个adminRealm来处理admin登录认证;userRealm来处理user登录认证,那么我们就需要进行多Realm配置
通过分析认证流程可得,Realm的操作是在ModularRealmAuthenticator的authenticate方法中 所以我们若是需要对realm进行操作,那么就需要继承ModularRealmAuthenticator重写authenticate方法。
实现思路
- 编写adminRealm、userRealm
- 扩展UsernamePasswordToken
- 重写authenticate方法
- 串联自动义类
- 测试
① 编写adminRealm、userRealm
adminRealm
package com.llz;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
public class AdminRealm extends AuthorizingRealm {
public String getName(){
return "admin";
}
// 授权方法
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
return null;
}
// 认证方法
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
// 参数就是我们subject.login(token)时传递的UsernamePasswordToken
...
// 当传入数据Shiro会进行数据匹配与断言,当账号不正确时抛出UnknownAccountException 密码不正确时抛出IncorrectCredentialsException
SimpleAuthenticationInfo authenticationInfo = null;
authenticationInfo = new SimpleAuthenticationInfo("数据库用户名", "123456", this.getName());
return authenticationInfo;
}
}
userRealm
package com.llz;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
public class UserRealm extends AuthorizingRealm {
public String getName(){
return "user";
}
// 授权方法
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
return null;
}
// 认证方法
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
// 参数就是我们subject.login(token)时传递的UsernamePasswordToken
...
// 当传入数据Shiro会进行数据匹配与断言,当账号不正确时抛出UnknownAccountException 密码不正确时抛出IncorrectCredentialsException
SimpleAuthenticationInfo authenticationInfo = null;
authenticationInfo = new SimpleAuthenticationInfo("数据库用户名", "123", this.getName());
return authenticationInfo;
}
}
② 扩展UsernamePasswordToken
为什么要扩展UsernamePasswordToken
在认证管理器的authenticate方法中,需要通过标记将realm分开,所以我们需要token携带登录类型。
只扩充一个标记
package com.llz;
import org.apache.shiro.authc.UsernamePasswordToken;
public class CustomerUsernamePasswordToken extends UsernamePasswordToken {
private final String TYPE_NAME;
public CustomerUsernamePasswordToken(String username,String password,String type_name) {
super(username,password);
TYPE_NAME = type_name;
}
public String getTYPE_NAME() {
return TYPE_NAME;
}
}
③ 重写authenticate方法
package com.llz;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.pam.ModularRealmAuthenticator;
import org.apache.shiro.realm.Realm;
import java.util.ArrayList;
import java.util.Collection;
public class CustomerModularRealmAuthenticator extends ModularRealmAuthenticator {
protected AuthenticationInfo doAuthenticate(AuthenticationToken authenticationToken) throws AuthenticationException {
this.assertRealmsConfigured();
Collection<Realm> realms = this.getRealms();
/**
* 开始修改
* 1. 获取token中的类型
* 2. 创建realm集合
* 3. 遍历所有的realm,找到类型批量的realm
* 4. 根据realm的个数编写返回值
*/
// 1.获取token中的类型
CustomerUsernamePasswordToken token = (CustomerUsernamePasswordToken) authenticationToken;
String typeName = token.getTYPE_NAME();
// 2. 创建realm集合
Collection<Realm> realmList = new ArrayList<>();
// 3. 创建符合请求类型的realm
for (Realm realm : realms) {
if (realm.getName().equals(typeName)){
realmList.add(realm);
}
}
// 4. 根据realm的个数编写返回值
return realmList.size() == 1 ? this.doSingleRealmAuthentication((Realm)realmList.iterator().next(), authenticationToken) : this.doMultiRealmAuthentication(realmList, authenticationToken);
}
}
④ 串联自动义类
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.mgt.DefaultSecurityManager;
import org.apache.shiro.realm.Realm;
import org.apache.shiro.subject.Subject;
import java.util.ArrayList;
public class TestDome01 {
public static void main(String[] args) {
// 1.创建默认安全管理器
DefaultSecurityManager securityManager = new DefaultSecurityManager();
// 2. 设置自定义认证器
CustomerModularRealmAuthenticator realmAuthenticator = new CustomerModularRealmAuthenticator();
// 设置多realm认证策略 当前环境为单realm,没必要设置
// realmAuthenticator.setAuthenticationStrategy(new AllSuccessfulStrategy());
securityManager.setAuthenticator(realmAuthenticator);
// 3. 设置Realm
ArrayList<Realm> realms = new ArrayList<>();
// admin类型
realms.add(new AdminRealm());
// user类型
realms.add(new UserRealm());
securityManager.setRealms(realms);
// 4. 给安全工具设置安全管理器
SecurityUtils.setSecurityManager(securityManager);
// 5. 通过安全工具获取主体(可以理解为登录用户的实体)
Subject subject = SecurityUtils.getSubject();
// 6.创建自定义登录令牌信息,传入登录类型
CustomerUsernamePasswordToken admin = new CustomerUsernamePasswordToken("zhangsan", "123456", "admin");
CustomerUsernamePasswordToken user = new CustomerUsernamePasswordToken("zhangsan", "1234", "user");
try {
// 7.登录验证
subject.login(admin);
// 8.获取验证状态
System.out.println(subject.isAuthenticated());
} catch (Exception e) {
e.printStackTrace();
}
}
}
⑤ 测试
- 单Realm测试
- 也就是每个类型的Realm只有一个
- 多Realm测试
- 传入多个admin类型的Realm测试(验证密码不同)
- 修改认证策略进行测试
第六部分 Spring Boot整合
SpringBoot+Shiro整合案例
0. 实现步骤
- 导入依赖
- 配置MyBatis-Plus配置
- Shiro配置(自定以Realm、设置安全管理器、设置资源过滤器)
- 编写实体类User以及登陆Controller进行搭建环境测试
- Utils准备(随即盐获取,以及ApplicationContext的Bean获取工具类)
- 完善注册操作
- 完善认证操作
- 完善授权操作
- 开启shiro缓存-EhCache
- 整合Redis缓存
1. 导入依赖
② 实现
导入依赖
<!--spring 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>
<!--lombok依赖-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!--测试-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!--shiro依赖-->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.4.1</version>
</dependency>
<!--持久层相关依赖-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.3.1</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.8</version>
</dependency>
2. MyBatis-Plus配置
spring:
thymeleaf:
prefix: classpath:/templates/
check-template-location: true
cache: false
suffix: .html
mode: HTML5
datasource:
username: root
password: root
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/test01?serverTimezone=GMT%2B8
type: com.alibaba.druid.pool.DruidDataSource
mybatis-plus:
mapper-locations: classpath:mapper/*.xml
type-aliases-package: com.example.xspringboot.entity
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
3. Shiro配置
① 自定义Realm
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
public class CustomerRealm extends AuthorizingRealm {
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
System.out.println("授权");
//TODO:授权(使用数据库数据)
return null;
}
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
//TODO:认证(使用数据库数据)
System.out.println("简单认证");
return null;
}
}
② Shiro配置
import org.apache.shiro.realm.Realm;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.HashMap;
@Configuration
public class ShiroConfig {
// 把自定义的Realm加入容器
@Bean
public Realm realm(){
return new CustomerRealm();
}
// 创建适用于Web的 SecurityManager
@Bean
public DefaultWebSecurityManager defaultWebSecurityManager(Realm realm){
DefaultWebSecurityManager defaultWebSecurityManager = new DefaultWebSecurityManager();
defaultWebSecurityManager.setRealm(realm);
return defaultWebSecurityManager;
}
// 创建过滤器 这是web必备的 在这里设置那些资源需要拦截(还可以设置拦截类型)
@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean(DefaultWebSecurityManager defaultWebSecurityManager){
ShiroFilterFactoryBean factoryBean = new ShiroFilterFactoryBean();
factoryBean.setSecurityManager(defaultWebSecurityManager);
HashMap<String, String> hashMap = new HashMap<>();
//hashMap.put("/index","anon");
hashMap.put("/**","authc");
factoryBean.setFilterChainDefinitionMap(hashMap);
factoryBean.setLoginUrl("/login");
return factoryBean;
}
}
4. 初步调试
在这里调试系统是否搭建成功,能否链接到数据库什么的
① User
根据数据表进行实体类编写,省略User相关的service、mapper、mapper.xml代码(基本的增删改查)
@Data
public class User {
private Long id;
// name存放随机盐 命名错了
private String name;
private String account;
private String password;
private LocalDateTime createTime;
private Integer stateFlag;
// 省略构造方法
}
② 测试controller
@RequestMapping("/index")
public ModelAndView index(ModelAndView modelAndView,String username,String password){
Subject subject = SecurityUtils.getSubject();
try {
subject.login(new UsernamePasswordToken(username,password));
}catch (UnknownAccountException e){
System.out.println("账号错误!!!");
}catch (IncorrectCredentialsException e){
System.out.println("密码错误!!!");
}
modelAndView.setViewName("index");
return modelAndView;
}
// 因为Realm我们并没有配置所以一定报错,只要打印出认证就算成功 页面省略,访问任意路径都会跳转到login页面需要认证才能访问index
调通之后可进行下一步
5. 工具类准备
① Utils
获取随机盐
@Component
public class Utils {
// 将散列次数定义为常量
public static final int HASH_CODE = 1024;
public static String getSalt(int i){
char[] charArray = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789~!@#$%^&*()_+".toCharArray();
StringBuilder stringBuffer = new StringBuilder();
for (int j = 0; j < i; j++) {
stringBuffer.append(charArray[new Random().nextInt(charArray.length)]);
}
return stringBuffer.toString();
}
}
② ApplicationContextUtils
@Component
public class ApplicationContextUtils implements ApplicationContextAware {
private static ApplicationContext context;
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.context = applicationContext;
}
// 用来获取指定的Bean
public static Object getBean(String beanName){
return context.getBean(beanName);
}
}
6. 完善 注册操作
① 注册
@RequestMapping("/register")
public ModelAndView register(ModelAndView modelAndView,String account,String password){
// 获取随机盐
String salt = Utils.getSalt(4);
// 加密密码
Md5Hash md5Hash = new Md5Hash(password, salt,Utils.HASH_CODE);
// 保存用户信息
service.save(new User(account,md5Hash.toHex(),salt));
modelAndView.setViewName("login");
return modelAndView;
}
② Realm设置加密方法
也就是修改ShiroConfig中的Realm
@Bean
public Realm realm(){
// 设置加密方法
HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
hashedCredentialsMatcher.setHashAlgorithmName("md5");
hashedCredentialsMatcher.setHashIterations(Utils.HASH_CODE);
CustomerRealm customerRealm = new CustomerRealm();
customerRealm.setCredentialsMatcher(hashedCredentialsMatcher);
return customerRealm;
}
7. 完善认证方法
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
System.out.println("认证开始");
// 获取账号
String account = (String) authenticationToken.getPrincipal();
// 获取UserService
UserService userServiceImpl = (UserService)ApplicationContextUtils.getBean("userServiceImpl");
// 获取User
User user = userServiceImpl.getUserByAccount(account);
// 认证
if (user != null){
return new SimpleAuthenticationInfo(user.getAccount(),user.getPassword(), ByteSource.Util.bytes(user.getName()),this.getName());
}
return null;
}
8. 完善授权方法
① 设置用户
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
System.out.println("授权");
// 获取用户账号
String primaryPrincipal = (String) principalCollection.getPrimaryPrincipal();
// 获取UserService
UserService userServiceImpl = (UserService)ApplicationContextUtils.getBean("userServiceImpl");
// 获取用户权限信息
List<Permission> permissionList = userServiceImpl.getPermissionByAccount(primaryPrincipal);
// 设置权限
if (!permissionList.isEmpty()){
SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
permissionList.forEach(po ->{
simpleAuthorizationInfo.addStringPermission(po.getValue());
});
return simpleAuthorizationInfo;
}
return null;
}
② 设置资源访问权限-subject
使用subject进行资源权限确认
@RequestMapping("/list")
public String list(){
Subject subject = SecurityUtils.getSubject();
boolean permitted = subject.isPermitted("llz:product:list");
if (permitted){
...
}
return "商品列表";
}
② 设置资源访问权限-注解
使用注解方式设置资源访问权限
@RequestMapping("/save")
@RequiresPermissions("llz:product:save") // 需要开启注解方式
public String save(){
return "商品保存";
}
开启注解方式
在ShiroConfig中配置
/**
* 开启Shiro的注解(如@RequiresRoles,@RequiresPermissions),需借助SpringAOP扫描使用Shiro注解的类,并在必要时进行安全逻辑验证
* 配置以下两个bean(DefaultAdvisorAutoProxyCreator和AuthorizationAttributeSourceAdvisor)即可实现此功能
*/
@Bean
public DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator(){
DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
advisorAutoProxyCreator.setProxyTargetClass(true);
return advisorAutoProxyCreator;
}
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(DefaultWebSecurityManager defaultWebSecurityManager) {
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
authorizationAttributeSourceAdvisor.setSecurityManager(defaultWebSecurityManager);
return authorizationAttributeSourceAdvisor;
}
/**
* 配置异常处理 必须是HandlerExceptionResolver接口实现类
* @return
*/
@Bean
public SimpleMappingExceptionResolver simpleMappingExceptionResolver(){
SimpleMappingExceptionResolver simpleMappingExceptionResolver =
new SimpleMappingExceptionResolver();
Properties properties = new Properties();
//配置当出现未授权异常时,跳转地址,只依赖shiro的配置不会跳转
properties.put("org.apache.shiro.authz.UnauthorizedException","/html/unauthorized.html");
simpleMappingExceptionResolver.setExceptionMappings(properties);
return simpleMappingExceptionResolver;
}
9. 开启Shiro - EhCache缓存
EhCache缓存特点:程序内缓存,程序终止缓存消失
① 引入依赖
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-ehcache</artifactId>
<version>1.5.3</version>
</dependency>
② 开启缓存
为Realm设置开启缓存
@Bean
public Realm realm(){
// 设置加密方法
HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
hashedCredentialsMatcher.setHashAlgorithmName("md5");
hashedCredentialsMatcher.setHashIterations(Utils.HASH_CODE);
CustomerRealm customerRealm = new CustomerRealm();
customerRealm.setCredentialsMatcher(hashedCredentialsMatcher);
// 开启shiroEhCacheManager
customerRealm.setCacheManager(new EhCacheManager()); // 配置缓存管理器
customerRealm.setCachingEnabled(true); // 启用全局缓存
customerRealm.setAuthenticationCachingEnabled(true); // 启用登录验证缓存
customerRealm.setAuthorizationCachingEnabled(true); // 启用授权认证缓存
customerRealm.setAuthenticationCacheName("authentication_cache"); // 为登录验证缓存命名
customerRealm.setAuthorizationCacheName("authorization_cache"); // 为授权认证缓存命名
return customerRealm;
}
10. redis缓存
redis缓存不会因程序终结而消失
① redis配置
引入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
// 版本默认和spring一致 也可以自己指定版本
yml配置
spring:
redis:
host: localhost
port: 6379
# 使用的数据库
database: 0
# 链接超时时间
# connect-timeout:
lettuce:
pool:
# 最大连接数
max-active: 20
# 最大等待时间 负数为永久
max-wait: -1
# 最大空闲连接数
max-idle: 5
# 最小空闲连接数
min-idle: 0
② 定义 RedisCacheManager
通过代码发现,shiro开启缓存使用的是一个shiroEhCacheManager,所以要开启redis缓存就要创建自己的RedisCacheManager
import org.apache.shiro.cache.Cache;
import org.apache.shiro.cache.CacheException;
import org.apache.shiro.cache.CacheManager;
// 实现shior指定的缓存管理器接口
public class ShiroCacheManager implements CacheManager {
@Override
public <K, V> Cache<K, V> getCache(String s) throws CacheException {
// 如果是认证的缓存 s=authentication_cache
// 如果是授权的缓存 s=authorization_cache
return new RedisCache<K,V>(s);
}
}
通过返回值我们得知,我们需要返回一个实现Cache<K, V>接口的类
③ RedisCache<K, V>
我们需要shiro缓存规定的 Cache<K, V>接口
import com.example.xspringboot.common.ApplicationContextUtils;
import org.apache.shiro.cache.Cache;
import org.apache.shiro.cache.CacheException;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import java.util.Collection;
import java.util.Set;
// 自定义redis缓存器 最主要的就是get put remove
public class RedisCache<K, V> implements Cache<K, V> {
// 缓存名称,我们将认证和授权名称作为参数传递,方便后续指定缓存key
private final String cacheName;
public RedisCache(String cacheName) {
this.cacheName = cacheName;
}
// 当触发认证或授权 会自动调用get方法 参数k就是我们的principal(就是账号了)
@Override
public V get(K k) throws CacheException {
RedisTemplate<String, V> template = getTemplate();
template.setKeySerializer(new StringRedisSerializer());
return (V) template.opsForHash().get(cacheName, k.toString());
}
// 触发缓存时 会使用put方法进行数据存储 所以我们要在这个方法内完成redis数据添加,这样就可以get到了
@Override
public V put(K k, V v) throws CacheException {
// k就是我们的账号 v就是我们要缓存的数据(认证时是密码,授权时是权限列表)
RedisTemplate<String, V> template = getTemplate();
template.setKeySerializer(new StringRedisSerializer());
template.setHashKeySerializer(new StringRedisSerializer());
template.opsForHash().put(cacheName, k.toString(), v);
return null;
}
// 当使用 subject.logout(); 方法时会调用该方法
@Override
public V remove(K k) throws CacheException {
RedisTemplate<String, V> template = getTemplate();
template.opsForHash().delete(cacheName,k.toString());
return null;
}
// 清除指定key...
@Override
public void clear() throws CacheException {
RedisTemplate<String, V> template = getTemplate();
template.delete(cacheName);
}
@Override
public int size() {
RedisTemplate<String, V> template = getTemplate();
return template.opsForHash().size(cacheName).intValue();
}
@Override
public Set<K> keys() {
RedisTemplate<String, V> template = getTemplate();
return (Set<K>) template.opsForHash().keys(cacheName);
}
@Override
public Collection<V> values() {
RedisTemplate<String, V> template = getTemplate();
return (Collection<V>) template.opsForHash().values(cacheName);
}
// 多个地方使用RedisTemplate所以封装为一个方法 当然也可以象cacheName那样作为一个属性,这样就不用写这些了
private RedisTemplate<String, V> getTemplate() {
return (RedisTemplate<String, V>) ApplicationContextUtils.getBean("redisTemplate");
}
}
为什么要使用Hash呢?
- 我们要进行 认证和授权 的缓存,但我们的k又是相同的,所以必须用一个更大的k来区分他是缓存还是认证
- 我们要缓存的用户不止一个,是一个列表
- 综上所述我们要使用hash,大k就是cacheName,这也是我们用他的原因
④ ByteSource序列化处理
修改原因
- 因为密码加密使用了 ByteSource进行随机盐转换
- redis保存需要序列化,官方ByteSource没有序列化
- 所以缓存时会出异常,所以需要我们自定义ByteSource
代码实现
就一模板也不用记
import org.apache.shiro.codec.Base64;
import org.apache.shiro.codec.CodecSupport;
import org.apache.shiro.codec.Hex;
import org.apache.shiro.util.ByteSource;
import java.io.File;
import java.io.InputStream;
import java.io.Serializable;
import java.util.Arrays;
public class MyByteSource implements ByteSource, Serializable {
private static final long serialVersionUID = 5175082362119580768L;
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 MyDataSource.BytesHelper()).getBytes(file);
}
public MyByteSource(InputStream stream) {
this.bytes = (new MyDataSource.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 void setBytes(byte[] bytes) {
this.bytes = bytes;
}
@Override
public byte[] getBytes() {
return this.bytes;
}
@Override
public String toHex() {
if (this.cachedHex == null) {
this.cachedHex = Hex.encodeToString(this.getBytes());
}
return this.cachedHex;
}
@Override
public String toBase64() {
if (this.cachedBase64 == null) {
this.cachedBase64 = Base64.encodeToString(this.getBytes());
}
return this.cachedBase64;
}
@Override
public boolean isEmpty() {
return this.bytes == null || this.bytes.length == 0;
}
@Override
public String toString() {
return this.toBase64();
}
@Override
public int hashCode() {
return this.bytes != null && this.bytes.length != 0 ? Arrays.hashCode(this.bytes) : 0;
}
@Override
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的地方改为自己的ByteSource
修改处在认证方法的最后一步
...
return new SimpleAuthenticationInfo(user.getAccount(),user.getPassword()
,new MyByteSource(user.getName()).bytes() // 修改
,this.getName());
...
null) {
this.cachedBase64 = Base64.encodeToString(this.getBytes());
}
return this.cachedBase64;
}
@Override
public boolean isEmpty() {
return this.bytes == null || this.bytes.length == 0;
}
@Override
public String toString() {
return this.toBase64();
}
@Override
public int hashCode() {
return this.bytes != null && this.bytes.length != 0 ? Arrays.hashCode(this.bytes) : 0;
}
@Override
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的地方改为自己的ByteSource**
修改处在认证方法的最后一步
```java
...
return new SimpleAuthenticationInfo(user.getAccount(),user.getPassword()
,new MyByteSource(user.getName()).bytes() // 修改
,this.getName());
...