首先开始前,在这里吹个牛,如果愿意仔细花时间看完这篇文章,如果还不会shiro,直播吃屎(就是这么自信)
本文代码示例已放入github:请点击我
快速导航-------->src.main.java.yq.Shiro
1.Apache Shiro是什么?
答:ApacheShiro是Java安全框架,执行身份验证、授权、密码和会话管理
2.为什么使用Apache Shiro?
答:Apache Shiro功能强大,使用简单,快速上手而且相对独立,不依赖其他框架,从最小的移动应用程序到最大的网络和企业应用程序都可以使用Shiro作为安全框架。
3.怎么使用Apache Shiro?
首先项目主要技术:Springboot2.1.6,shiro1.3.2,jjwt0.7.0,Jpa,Mysql等
其中jjwt(Json Web Token)我用来生产登录token以及密码加密和解密
其次就是该Dome的模式采用Token模式,也就是用户登录之后会返回一个Token,后续关键请求基于Token进行身份验证,从而达到取代session的作用
在使用之前我们先了解一下Shiro的主要功能,以及执行流程
Shiro三大核心组件:Subject SecurityManager Realms.
Subject:即“当前操作用户”。但是,在Shiro中,Subject这一概念并不仅仅指人,也可以是第三方进程、后台帐户(Daemon Account)或其他类似事物。它仅仅意味着“当前跟软件交互的东西”。(获取用户信息,用户实例)
SecurityManager:它是Shiro框架的核心,典型的Facade模式,Shiro通过SecurityManager来管理内部组件实例,并通过它来提供安全管理的各种服务。(核心,中央处理器)
Realm: Realm充当了Shiro与应用安全数据间的“桥梁”或者“连接器”。也就是说,当对用户执行认证(登录)和授权(访问控制)验证时,Shiro会从应用配置的Realm中查找用户及其权限信息。(也就是管理用户登录和授权)
从这个意义上讲,Realm实质上是一个安全相关的DAO:它封装了数据源的连接细节,并在需要时将相关数据提供给Shiro。当配置Shiro时,你必须至少指定一个Realm,用于认证和(或)授权。配置多个Realm是可以的,但是至少需要一个
授权流程图:
接下来我开始详细讲解:
- 1.创建我们的SpringBoot项目以及加入核心依赖
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>yq</groupId>
<artifactId>test</artifactId>
<version>1.0-SNAPSHOT</version>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.6.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<dependencies>
<!-- 引入jwt依赖 使用jwt协议进行单点token登录 -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.7.0</version>
</dependency>
<!-- 引入shiro -->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.3.2</version>
</dependency>
<!-- <!– https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-security –>-->
<!-- <dependency>-->
<!-- <groupId>org.springframework.boot</groupId>-->
<!-- <artifactId>spring-boot-starter-security</artifactId>-->
<!-- </dependency>-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<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>
<!-- https://mvnrepository.com/artifact/org.apache.commons/commons-lang3 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.8.1</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.30</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<!-- <!–orcale数据库–>-->
<!-- <!– https://mvnrepository.com/artifact/com.jslsolucoes/ojdbc6 –>-->
<!-- <dependency>-->
<!-- <groupId>com.jslsolucoes</groupId>-->
<!-- <artifactId>ojdbc6</artifactId>-->
<!-- <version>11.2.0.1.0</version>-->
<!-- </dependency>-->
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
<resources>
<!--非class应均在该目录下-->
<resource>
<directory>src/main/resources</directory>
<filtering>false</filtering>
</resource>
</resources>
</build>
</project>
这里我就不多介绍jar的的作用了
- 2.搭建一个Web项目的相关准备
/*
Navicat Premium Data Transfer
Source Server : 192.168.0.21
Source Server Type : MySQL
Source Server Version : 80015
Source Host : 192.168.0.21:3306
Source Schema : workorder
Target Server Type : MySQL
Target Server Version : 80015
File Encoding : 65001
Date: 12/08/2019 16:31:47
*/
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- Table structure for test
-- ----------------------------
DROP TABLE IF EXISTS `test`;
CREATE TABLE `test` (
`id` bigint(20) NOT NULL,
`user_name` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`phone` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`pass_word` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`role` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`turisdiction` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of test
-- ----------------------------
INSERT INTO `test` VALUES (16168479940608, '张三', '17549684489', 'eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjM0NTYifQ.q5OCp5vPp2XnwLCxqcxexnu341YbNd0987xJiVY_Qew', 'admin', 'all');
INSERT INTO `test` VALUES (16168534069248, '李四', '17549684489', 'eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjM0NTYifQ.q5OCp5vPp2XnwLCxqcxexnu341YbNd0987xJiVY_Qew', 'user', 'check');
SET FOREIGN_KEY_CHECKS = 1;
这是一个数据库的表的信息,如下图:
至于字段信息请看下面的实体类:
@Data
@Entity
public class Test {
@Id
private Long id; //数据库主键
private String phone; //电话号码
private String passWord; //密码
private String userName; //用户名
private String role; //角色
private String turisdiction; //权限 使用英文 , 隔开
}
现在我们有了实体类,有了数据库,就开始创建一个dao层和Controller层,至于service层不是重点,就不写出来了
这就是我们的dao层,因为项目简单就不用拓展接口,直接使用jpa自带的完全满足业务需求
@Repository
public interface MySqlMapper extends JpaRepository<Test,Long> {
}
由于我们在数据库中的密码进行了加密,而且我们要生产我们的Token所以这里我们封装了使用jwt对字符串加密的一个服务类
/**
* 使用jjwt实现的token生成策略 以及密码加密策略
*/
@Slf4j
public class TokenService {
// "iss":"Issuer —— 用于说明该JWT是由谁签发的",
// "sub":"Subject —— 用于说明该JWT面向的对象",
// "aud":"Audience —— 用于说明该JWT发送给的用户",
// "exp":"Expiration Time —— 数字类型,说明该JWT过期的时间",
// "nbf":"Not Before —— 数字类型,说明在该时间之前JWT不能被接受与处理",
// "iat":"Issued At —— 数字类型,说明该JWT何时被签发",
// "jti":"JWT ID —— 说明标明JWT的唯一ID",
// "user-definde1":"自定义属性举例",
// "user-definde2":"自定义属性举例"
//读取配置文件 秘匙 (这是用来后面接收到token的时候用于解密用的秘匙
private String secretKey;
//过期时间 两周
private Long outTime_towWeeks;
@Autowired
private Environment environment;
@PostConstruct
private void inir() {
this.secretKey = environment.getProperty("secretKey");
Integer outTime = Integer.parseInt(environment.getProperty("outTime"));
//过期时间两周
this.outTime_towWeeks = outTime * 1000L * 60 * 60 * 24;
log.info("JWTTokenUtil初始化完成,secretKey为:{} ,loginToken过期时间为:{}", secretKey, outTime_towWeeks);
}
/**
* 字符串加密 如果参数type不是null 那么就是用户token生成。那是需要过期时间的
* 如果type为null 那么就是密码加密 是不需过期时间的
* @param subject 传递的字符串
* @param type 需要加密类型 如果
* @return
*/
private String createJWT(String subject,String type) {
//加密算法
SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
byte[] apiKeySecretBytes = DatatypeConverter.parseBase64Binary(secretKey);
Key signingKey = new SecretKeySpec(apiKeySecretBytes, signatureAlgorithm.getJcaName());
//创建jwt对象
JwtBuilder builder = Jwts.builder()
.setSubject(subject)
.signWith(signatureAlgorithm, signingKey);
//如果是null 就表示是密码加密 密码加密是不需要执行过期时间的
if(StringUtils.isEmpty(type)){
return builder.compact();
}
//设置两周之后过期
builder.setExpiration(new Date(System.currentTimeMillis()+outTime_towWeeks));
return builder.compact();
}
/**
* token解密过程
* @param jwtToken token
* @return 解密后的值
*/
public String parseJWT(String jwtToken) {
Claims claims = Jwts.parser()
.setSigningKey(DatatypeConverter.parseBase64Binary(secretKey))
.parseClaimsJws(jwtToken).getBody();
Date expiration = claims.getExpiration();
//表示没有设置过期时间
if(expiration == null){
return claims.getSubject();
}
//表示已经过期
if(System.currentTimeMillis() >= expiration.getTime()){
throw new DIYException("token已经过期");
}
return claims.getSubject();
}
/**
* 用户密码加密
* @param passWord 原密码
* @return 加密之后的密码
*/
public String passWordEncryption(String passWord){
return createJWT(passWord, null);
}
/**
* 创建用户token
* @param param 需要封装的参数的String类型
* @return 生成的用户token
*/
public String createToken(String param){
return createJWT(param, "create");
}
}
这就是我们的对字符串加密解密以及生产token的服务类了,但是我们这里没有马上使用@Service注入到容器之中,至于为什么,我们后面会讲到
这里会读取两个配置文件,如下:
secretKey: myKey
#过期时间 单位天数
outTime: 15
接下来我们创建我们的Controller层
@RestController
public class TestShiroController extends BaseApiService {
@Autowired
private TokenService tokenService;
@Autowired
private MySqlService mySqlService;
/*
用户登录 需要传递用户邮箱和密码
*/
@PostMapping(value = "/user/login")
public ResponseBase login(@RequestBody Test test) {
//根据id查询Test
Test testById = mySqlService.getTestById(test.getId());
//判断不能为null
Assert.notNull(testById,"用户账号错误");
//获取到加密之后的password
String encryptionPassWord = tokenService.parseJWT(testById.getPassWord());
//密码判断
if(! test.getPassWord().equals(encryptionPassWord)){
throw new IllegalArgumentException("密码错误");
}
Subject subject = SecurityUtils.getSubject();
//设置登录token 过期时间为30分钟
String token = tokenService.createToken(JSON.toJSONString(testById));
//这个类 是我们继承与shiro的AuthenticationToken 这样就可以做一些定制化的东西
NewAuthenticationToken newAuthenticationToken = new NewAuthenticationToken(testById.getPhone(), token);
//登录操作
subject.login(newAuthenticationToken);
//返回客户端数据
JSONObject jsonObject = new JSONObject();
jsonObject.put(AuthFilter.TOKEN, token);
return setResultSuccessData(jsonObject.toString(), "用户登录成功");
}
@PostMapping(value = "/api/test001")
public ResponseBase test001(){
return setResultSuccess("测试登录成功");
}
//测试权限使用
@PostMapping(value = "/api/testRole")
public ResponseBase testRole(){
return setResultSuccess("测试角色成功");
}
//测试权限使用
@PostMapping(value = "/api/testPerms")
public ResponseBase testPerms(){
return setResultSuccess("测试权限成功");
}
}
可以看到我们这里有四个接口,很简单的接口,分别是测试登录。验证是否登录,以及测试角色,和测试权限的四个接口
在这里登录的时候会使用到一个类 NewAuthenticationToken 这个类是我们自定义的但是是继承与shiro的AuthenticationToken类,为什么要继承他呢,这样我们就可以更加透明化的知道shiro登录的流程,以及可以定制化一些东西
shiro登录流程:subject.login(AuthenticationToken authenticationToken) --> realm.doGetAuthenticationInfo(AuthenticationToken authenticationToken)
为什么需要这么一个东西呢(AuthenticationToken):我们可以点进去看源码的注释,简单点说,这个东西就是在我们执行了subject.login()方法之后会执行MyRealm的doGetAuthenticationInfo方法进行登陆,而进行获取证明身份的数据
(自定义的NewAuthenticationToken 以及 Realm 这个我们后面讲,我们先从简单的零件讲)
好了,到了这里我们准备工作做完了,name马上涉及到Shiro最核心的部分了
- 3.开始搭建shiro
首先我们创建一个 NewAuthenticationToken 继承于 AuthenticationToken 为了就是定制化以及更加了解shiro登录流程
/**
* 用户身份验证的凭证
*/
@Data
//生成默认构造器
@NoArgsConstructor
//生产带所有属性的构造器
@AllArgsConstructor
public class NewAuthenticationToken implements AuthenticationToken {
private String phone;
private String token;
//得到主体
@Override
public Object getPrincipal() {
return this.phone;
}
//得到凭证
@Override
public Object getCredentials() {
return this.token;
}
}
这个类一出来,应该就明白了为什么在Controller的时候我们传递了一个用户的电话号码(因为是唯一的)和一个用户登录的token了吧,但是作用呢,我们后面再讲。
NewAuthenticationToken newAuthenticationToken = new NewAuthenticationToken(testById.getPhone(), token);
接下来我们创建一个shiro的三大核心之一的MyRealm
@Service
public class MyRealm extends AuthorizingRealm {
@Autowired
private MySqlService mySqlService;
@Autowired
private TokenService tokenService;
/**
* 大坑!,必须重写此方法,不然Shiro会报错
*/
@Override
public boolean supports(AuthenticationToken token) {
return token instanceof NewAuthenticationToken;
}
/**
* 保存角色和权限
* @param principals
* @return
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
Long testId = (Long) principals.getPrimaryPrincipal();
Test testById = mySqlService.getTestById(testId);
if(testById == null){
throw new IllegalArgumentException("错误的角色");
}
//在这里给用户角色进行授权
//在这里拿到用户的信息 并且赋值角色和权限
SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
//设置用户角色
simpleAuthorizationInfo.addRole(testById.getRole());
//添加角色的权限
simpleAuthorizationInfo.addStringPermissions(Arrays.asList(testById.getTurisdiction().split(",")));
return simpleAuthorizationInfo;
}
/**
* 身份认证
* @param auth
* @return
* @throws AuthenticationException
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken auth) throws AuthenticationException {
//因为我们在用户登录的时候传递的参数 主体就是电话号码
String phone = auth.getPrincipal().toString();
//证明用户信息的东西
String token = auth.getCredentials().toString();
//因为我们传递的是json类型的Test对象
String jsonTest = tokenService.parseJWT(token);
Test test = JSON.parseObject(jsonTest, Test.class);
if(! test.getPhone().equals(phone)){
throw new AuthenticationException("用户身份验证失败");
}
//保存用户信息?test, token, "my_realm"
return new SimpleAuthenticationInfo(test.getId(),token,"myRealm");
}
}
这个类非常重要,首先我们看到了两个方法,这两个方法是非常重要的
doGetAuthorizationInfo:该方法就是用来对登录的用户进行角色赋值和权限赋值的,这个方法不会立马执行,会在进行角色判断或者权限判断的时候才执行该方法。这里我们就暂时不说。
doGetAuthenticationInfo:该方法就是用来登录的,在使用subject.login的时候会调用该方法,从代码中可以看到我们可以使用AuthenticationToken这个类调用getPrincipal方法获取主体信息,getCredentials方法获取凭证信息,而我们就可以利用该信息进行刚刚登录的用户身份验证(我这里觉得这个身份验证不是很有必要,因为我们在Controller已经进行了身份验证)
在执行了doGetAuthenticationInfo方法的时候我们看到了如下代码
return new SimpleAuthenticationInfo(test.getId(),token,"myRealm");
那么返回的这个对象又是干什么的呢?我们点进去可以看到:
简单点说,这个对象就是保存的我们的用户登录的信息,第一个参数同样是主体,第二个参数同样是证明,第三个参数就是使用的什么 realm 那么他作用是什么?说简单点,他的作用就是我们在后面进行身份验证的时候可以使用subject.getPrincipal()进行判断用户是否登录。
所以看到这里,我们大致理一下shiro是怎么登录的,又是怎么判断用户登录的:
首先使用subject.login(authenticationToken)方法调用realm中的doGetAuthenticationInfo方法进行获取用户登录的信息进行身份验证,验证通过的时候保存到AuthenticationInfo的实现类SimpleAuthenticationInfo中的,然后我们就可以使用subject.getPrincipal()获取主体对象是否为null或者是我们指定的类型来判断用户是否登录
首先这里有两个问题是我也遇到的这里就为大家解读一下:
这里必读:
1.在用户身份验证成功的时候SimpleAuthenticationInfo的主体是传递username还是传递user,这是引用百度的上网友的问题,那么这里我们就应该是传递Id还是传递Test对象呢,这个还是根据情况来定,如果我们使用的shiro是的缓存是基于Redis的话,那么还是推荐是哟Id也就是唯一的主键进行保存为主体,但是我们这个项目的保存对象是基于session的,所以就对于主体保存test对象还是id主键没有太多要求,都可以,说实话直接保存test会方便很多,但是为了演示效果我们这里还是使用的保存id。
2.使用subject.getPrincipal()能返回当前的用户主体对象,那么问题来了,shiro是怎么知道返回的是哪个对象呢?这个问题就是shiro在登录的时候会把用户信息进行绑定到当前的线程中特就是threadLocal里面,在基于浏览器的cookie和session进行的身份验证,那么如果session和cookie失效了怎么办,所以这就是为什么还会有一个subject.credentials()得到用户凭证的原意。
具体想了解为什么上面两个原因可以自行百度,我们不过多详解,了解即可
好了我们realm完成了,接下来就是核心的shiroConfig了,也就是SecurityManager相关
@Configuration
public class ShiroConfig {
@Autowired
private TokenService tokenService;
@Bean
public TokenService tokenService(){
return new TokenService();
}
//常量一:表示是角色
public static final String CONS_TYPE_ONE = "ROLE";
//常量二:表示是权限
public static final String CONS_TYPE_TWO = "PERM";
/**
* 权限管理 核心安全事务管理器
* @param realm
* @return
*/
@Bean("securityManager")
public DefaultWebSecurityManager getManager(MyRealm realm) {
DefaultWebSecurityManager manager = new DefaultWebSecurityManager();
// 使用自己的realm
manager.setRealm(realm);
return manager;
}
//Filter工厂,设置对应的过滤条件和跳转条件
@Bean("shiroFilter")
public ShiroFilterFactoryBean factory(DefaultWebSecurityManager securityManager) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
shiroFilterFactoryBean.setSecurityManager(securityManager);
Map<String, Filter> myFilters = new HashMap<>();
myFilters.put("authFilter",new AuthFilter(tokenService()));
shiroFilterFactoryBean.setFilters(myFilters);
Map<String,String> map = new LinkedHashMap<>();
//用户登录 自由访问
map.put("/user/login","anon");
map.put("/static/**","anon");
//需要admin角色
map.put("/api/testRole","authFilter["+CONS_TYPE_ONE+",admin]");
//需要test权限才能访问
map.put("/api/testPerms","authFilter["+CONS_TYPE_TWO+",test]");
// shiroFilterFactoryBean.setUnauthorizedUrl("/user/error");
//其他的api请求都需要认证
map.put("/api/**","authFilter");
shiroFilterFactoryBean.setFilterChainDefinitionMap(map);
return shiroFilterFactoryBean;
}
/**
* 下面的代码是添加注解支持 aop(用于解决注解不生效的原因
*/
@Bean
@DependsOn("lifecycleBeanPostProcessor")
public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
defaultAdvisorAutoProxyCreator.setProxyTargetClass(true);
return defaultAdvisorAutoProxyCreator;
}
/**
* 管理shirobean的生命周期
* @return
*/
@Bean
public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
return new LifecycleBeanPostProcessor();
}
/**
* 加入注解
* @param securityManager
* @return
*/
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(DefaultWebSecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
advisor.setSecurityManager(securityManager);
return advisor;
}
}
这里需要说的事情不多,首先就是配置securityManager,指定我们自己realm进行认证
其次就是我们的shiroFilter过滤器的配置
这里我们的角色认证和权限认证,以及身份验证都是使用的自定义的authFilter进行实现的,当然也可以使用shiro自带的过滤器进行实现,但是为了更好的理解shiro的认证流程和原理我还是使用了自定义的filter进行实现该功能
shior几大拦截器:https://blog.youkuaiyun.com/fenglixiong123/article/details/77119857
可以参考该文章了解一下shiro的几大拦截器的作用以及怎么配置,
从我们的shiroFilter中我们可以看到我们配置了:
/user/login anon :意思就是不需要身份验证,都可以进行访问
/api/testRole authFilter["+CONS_TYPE_ONE+",admin] : 意思就是这个接口需要admin的角色才可以访问,至于为什么要这么写,因为我们这哥filter过滤器实现了三种功能,分别是身份验证,和角色验证以及权限认证,所以我们只能根据[]内的第一个参数进行判断是角色认证还是权限认证,所以第一个参数是写死的,同样我们可以添加多种角色和权限,只需要使用英文的逗号进行隔开 所以这就是我们这样写的作用
/api/** authFilter :表示以api开头的接口需要进行身份验证,这里同样使用我们的自定义的接口
另外在这里我说一下有几个坑:
必看:
1.首先自定的filter过滤器不能使用@Service或者@Component交给Spring进行管理,这样会导致我们配置的过滤策略找不到,会报错:org.apache.shiro.UnavailableSecurityManagerException: No SecurityManager accessible to the calling code, either bound to the org.apache.shiro.util.ThreadContext or as a vm static singleton. This is an invalid application configuration.
所以要使用:
Map<String, Filter> myFilters = new HashMap<>();
myFilters.put("authFilter",new AuthFilter(tokenService()));
shiroFilterFactoryBean.setFilters(myFilters);
以下方式进行注入到shiro的filter管理器中,
2.那就是配置filter过滤策略的顺序 map.put("/api/**","authFilter"); --->一定要放在权限后面,不然会覆盖,导致我们的角色认证和权限认证失效。
3.为什么TokenService不在生成的时候使用注解@Component注入容器呢?因为我们自定的的AuthFilter过滤器会依赖这个服务,但是使用@Autowired会找不到,因为可能shiro的相关配置会先于spring的执行,具体原因有待发掘,所以我们这里只能使用AuthFilter的构造函数进行注入,然后在调用方法之后手动的把TokenService进行注入到容器之中
那么接下来就开启我们的手写过滤器实现权限认证,角色认证以及用户认证之AuthFilter
/**
* 用于角色身份验证
*/
@Slf4j
public class AuthFilter extends AuthorizationFilter {
//token
private final TokenService tokenService;
public AuthFilter(TokenService tokenService){
this.tokenService = tokenService;
System.out.println(tokenService);
}
public static final String TOKEN = "token";
private Subject getSubject(){
return SecurityUtils.getSubject();
}
/**
* 身份认证方法
*/
private Boolean authorization(HttpServletRequest request, HttpServletResponse response) {
try {
//获取token并解析
String token = request.getHeader(TOKEN);
Assert.hasLength(token,"token不能为空");
String jsonTest = tokenService.parseJWT(token);
Test test = JSON.parseObject(jsonTest, Test.class);
Assert.notNull(test,"错误的token");
Subject subject = getSubject();
//表示还没有登录 为什么这里要这么写 就是因为shiro我们的缓存是基于session,cookie等
//如果服务重启了 或者没了cookiet咋办,所以我们就在这里掉用一下shiro的登录
if(subject.getPrincipal() == null){
//那就登录
subject.login(new NewAuthenticationToken(test.getPhone(),token));
return true;
}
//如果是已经登录的 就进行身份验证
Long testId = (Long) subject.getPrincipal();
if(! test.getId().equals(testId)){
return false;
}
return true;
} catch (Exception e) {
log.error("用户验证失败的地址:{}",request.getRequestURL());
log.error("错误原因:{}",e.getMessage());
response.setHeader("messgae",e.getMessage());
return false;
}
}
/**
* 认证失败
* @param response
*/
private void authorizationFailure(HttpServletResponse response){
try{
//认证失败 之后返回页面的数据
response.setContentType("application/json;charset=utf-8");
//封装一个map返回页面
HashMap<Object, Object> result = new HashMap<>();
result.put("data","null");
result.put("message",response.getHeader("message"));
result.put("rtnCode","401");
response.getWriter().append(JSON.toJSONString(result));
}catch (Exception e){
log.error("响应错误:{}",e.getMessage());
}
}
/**
* 权限认证的方法
* @param perms
* @param response
* @param request
* @return
*/
private Boolean permissions(String[] perms,HttpServletResponse response,HttpServletRequest request){
Boolean result = false;
try{
Subject subject = getSubject();
//调用方法进行判断权限
if(! subject.isPermittedAll(perms)){
throw new Exception("您没有该访问权限");
}
result = true;
}catch (Exception e){
log.error("角色不对应导致无访问权限的地址:{}"+request.getRequestURL());
log.error("错误原因:{}",e.getMessage());
response.setHeader("message",e.getMessage());
}finally {
return result;
}
}
//角色认证认证
private Boolean roles(List<String> roles, HttpServletResponse response, HttpServletRequest request){
Boolean result = false;
try{
if(roles == null || roles.size() <= 0){
return true;
}
Subject subject = getSubject();
//调用方法判断 是否存在指定角色
if(! subject.hasAllRoles(roles)){
throw new Exception("没有该访问权限");
}
result = true;
}catch (Exception e){
log.error("没有权限的访问地址:{}",request.getRequestURL());
log.error("错误原因:{}",e.getMessage());
response.setHeader("message",e.getMessage());
}finally {
return result;
}
}
@Override
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
String[] values = (String[]) mappedValue;
HttpServletRequest newRequest = (HttpServletRequest) request;
HttpServletResponse newResponse = (HttpServletResponse) response;
Subject subject = getSubject();
//身份认证
if(values == null){
return authorization(newRequest,newResponse);
}
//表示是角色认证
if(values[0].equals(ShiroConfig.CONS_TYPE_ONE)){
//因为我们使用asList转换代码为List的时候不是util包下面的List而是array下面的,
// 所以我们需要转换为util包下的,才能执行remove方法
//那么为什么我们需要删除第0个呢?就是因为我们在shiroConfig的时候配置的过滤策略
//因为我们的自定的authFilte需要执行的认证种类太多,所以需要第一个参数进行判断类型,
//但是这第零个参数又是属于权限和角色范围,所以在类型判断之后需要删除
List<String> strings = Arrays.asList(values);
List<String> params = new ArrayList<>(strings);
params.remove(0);
//调用角色认证方法
return roles(params,newResponse,newRequest);
}
//权限认证
if(values[0].equals(ShiroConfig.CONS_TYPE_TWO)){
//同上 一样的意思
List<String> strings = Arrays.asList(values);
List<String> params = new ArrayList<>(strings);
params.remove(0);
//因为 我们的权限认证方法的参数是需要的是 String... 类型
// 但是String[] 有没有删除第一个的实现,所以就比较麻烦先转换list删除第一个,然后又转换回去
String[] perm = new String[params.size()];
String[] newPerm = params.toArray(perm);
//调用权限认证方法
return permissions(newPerm,newResponse,newRequest);
}
return false;
}
@Override
protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws IOException {
//如果权限或者角色也或者是角色认证不通过
authorizationFailure((HttpServletResponse) response);
return false;
}
}
我想我这里为什么需要这样写代码里面的注释已经写得很清楚了,但是我还是简单概括一下
1.为什么使用构造函数注入tokenService,因为使用@Autowired注解注入会找不到
2.为什么我们shiroConfig中的shiroFilter的关于权限和角色配置,会在authFilter的第一个参数传递两个常量,就是因为我们的过滤器是有三种功能,身份认证-->这个不需要参数,但是权限认证和角色是需要传递角色和权限的,而第一个常量就是用来区别到底是角色认证还是权限认证。但是常量又不是属于角色和权限里面的,所以在判断出是角色或者权限之后要删除掉第一个常量,角色和权限都可以传递多个参数,中间使用英文逗号分隔,在AuthFilter中的mappedValue参数就可以获取到我们在shiroFilter中配置过滤策略的时候传递的参数。
3.onAccessDenied:表示的是验证失败执行的地方,isAccessAllowed:是执行验证方法地方
到了这里基本上shiro的核心几个文件就讲的很清楚了,实际上很简单,就是两个都可以解决,那就是MyRealm和ShirlConfig
那到了这里我们的shiro的执行流程就很清晰了,那就是登录的时候使用subject,login()进行登路,然后会调用MyRealm中的doGetAuthorizationInfo进行身份验证,以及保存当前登录对象的一些信息,可以用来获取身份信息。
然后在需要角色认证或者权限认证的时候,首先活进入到Filter过滤器中,由于我们这里配置的是自定义的过滤器,所以在需要角色认证或者权限认证以及身份认证(是否登录)的时候会先进入到我们的AuthFilter过滤器中,然后判断是那种验证(身份,权限,以及角色)并执行响应的过滤流程,当然如果我们不适用自定义的,使用shiro自带的anon(不需要认证),authc(身份认证,也就是必须要登录),roles[?,?](角色认证),perms[?,?,?](权限认证)----->角色和权限都是需要使用引文逗号分隔
可以参考:https://blog.youkuaiyun.com/fenglixiong123/article/details/77119857 shiro几大拦截器
然后当需要角色或者权限认证的时候会执行MyReaml中的doGetAuthorizationInfo方法 就这样shiro的整个登录以及验证流程就完毕了。
接下来我们看一看我们执行的结果吧:
首先我们在我们的AuthFilter中的角色认证,权限认证,以及身份认证打上断点一会debug调试
好了,断点有了我们使用具有admin角色的账号登录:
发现我们的拦截器并没有进入?因为我们配置的/user/login的过滤策略是anon,表示不需要身份认证直接访问,而且我们看到了我们的登录返回的token
那么继续
我们测试带api开头的接口,因为我们过滤策略是api是需要登录的,我们先输入正确的token,debug停在了我们打断点需要执行身份验证的地方,而且返回值也是正常,
然后我们换成为user角色账号登录,并更换token重新发起请求发现:
同样的,权限判断是一个道理,这里就不掩饰了
最后送上我的shiro结构图:
这文章很长,看完需要不少的时间,但是如果您不会shiro我想您的收获会是很大的
~~~谢谢大家
本文代码示例已放入github:请点击我
快速导航-------->src.main.java.yq.Shiro