本文主要是介绍SpringBoot整合Shiro(看完不会,直播吃屎),希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!
首先开始前,在这里吹个牛,如果愿意仔细花时间看完这篇文章,如果还不会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 TransferSource Server : 192.168.0.21Source Server Type : MySQLSource Server Version : 80015Source Host : 192.168.0.21:3306Source Schema : workorderTarget Server Type : MySQLTarget Server Version : 80015File Encoding : 65001Date: 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 {@Idprivate 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;@Autowiredprivate Environment environment;@PostConstructprivate 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 {@Autowiredprivate TokenService tokenService;@Autowiredprivate MySqlService mySqlService;/*用户登录 需要传递用户邮箱和密码*/@PostMapping(value = "/user/login")public ResponseBase login(@RequestBody Test test) {//根据id查询TestTest testById = mySqlService.getTestById(test.getId());//判断不能为nullAssert.notNull(testById,"用户账号错误");//获取到加密之后的passwordString 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;//得到主体@Overridepublic Object getPrincipal() {return this.phone;}//得到凭证@Overridepublic Object getCredentials() {return this.token;}
}
这个类一出来,应该就明白了为什么在Controller的时候我们传递了一个用户的电话号码(因为是唯一的)和一个用户登录的token了吧,但是作用呢,我们后面再讲。
NewAuthenticationToken newAuthenticationToken = new NewAuthenticationToken(testById.getPhone(), token);
接下来我们创建一个shiro的三大核心之一的MyRealm
@Service
public class MyRealm extends AuthorizingRealm {@Autowiredprivate MySqlService mySqlService;@Autowiredprivate TokenService tokenService;/*** 大坑!,必须重写此方法,不然Shiro会报错*/@Overridepublic boolean supports(AuthenticationToken token) {return token instanceof NewAuthenticationToken;}/*** 保存角色和权限* @param principals* @return*/@Overrideprotected 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*/@Overrideprotected 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 {@Autowiredprivate TokenService tokenService;@Beanpublic 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();// 使用自己的realmmanager.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*/@Beanpublic LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {return new LifecycleBeanPostProcessor();}/*** 加入注解* @param securityManager* @return*/@Beanpublic AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(DefaultWebSecurityManager securityManager) {AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();advisor.setSecurityManager(securityManager);return advisor;}}
这里需要说的事情不多,首先就是配置securityManager,指定我们自己realm进行认证
其次就是我们的shiroFilter过滤器的配置
这里我们的角色认证和权限认证,以及身份验证都是使用的自定义的authFilter进行实现的,当然也可以使用shiro自带的过滤器进行实现,但是为了更好的理解shiro的认证流程和原理我还是使用了自定义的filter进行实现该功能
shior几大拦截器:https://blog.csdn.net/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 {//tokenprivate 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;}}@Overrideprotected 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;}@Overrideprotected 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.csdn.net/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
这篇关于SpringBoot整合Shiro(看完不会,直播吃屎)的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!