SpringBoot整合Shiro(看完不会,直播吃屎)

2024-02-14 09:40

本文主要是介绍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><!--        &lt;!&ndash; https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-security &ndash;&gt;-->
<!--        <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><!--        &lt;!&ndash;orcale数据库&ndash;&gt;-->
<!--        &lt;!&ndash; https://mvnrepository.com/artifact/com.jslsolucoes/ojdbc6 &ndash;&gt;-->
<!--        <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的核心几个文件就讲的很清楚了,实际上很简单,就是两个都可以解决,那就是MyRealmShirlConfig

那到了这里我们的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(看完不会,直播吃屎)的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/708144

相关文章

详解Java如何向http/https接口发出请求

《详解Java如何向http/https接口发出请求》这篇文章主要为大家详细介绍了Java如何实现向http/https接口发出请求,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... 用Java发送web请求所用到的包都在java.net下,在具体使用时可以用如下代码,你可以把它封装成一

SpringBoot使用Apache Tika检测敏感信息

《SpringBoot使用ApacheTika检测敏感信息》ApacheTika是一个功能强大的内容分析工具,它能够从多种文件格式中提取文本、元数据以及其他结构化信息,下面我们来看看如何使用Ap... 目录Tika 主要特性1. 多格式支持2. 自动文件类型检测3. 文本和元数据提取4. 支持 OCR(光学

Java内存泄漏问题的排查、优化与最佳实践

《Java内存泄漏问题的排查、优化与最佳实践》在Java开发中,内存泄漏是一个常见且令人头疼的问题,内存泄漏指的是程序在运行过程中,已经不再使用的对象没有被及时释放,从而导致内存占用不断增加,最终... 目录引言1. 什么是内存泄漏?常见的内存泄漏情况2. 如何排查 Java 中的内存泄漏?2.1 使用 J

JAVA系统中Spring Boot应用程序的配置文件application.yml使用详解

《JAVA系统中SpringBoot应用程序的配置文件application.yml使用详解》:本文主要介绍JAVA系统中SpringBoot应用程序的配置文件application.yml的... 目录文件路径文件内容解释1. Server 配置2. Spring 配置3. Logging 配置4. Ma

Java 字符数组转字符串的常用方法

《Java字符数组转字符串的常用方法》文章总结了在Java中将字符数组转换为字符串的几种常用方法,包括使用String构造函数、String.valueOf()方法、StringBuilder以及A... 目录1. 使用String构造函数1.1 基本转换方法1.2 注意事项2. 使用String.valu

java脚本使用不同版本jdk的说明介绍

《java脚本使用不同版本jdk的说明介绍》本文介绍了在Java中执行JavaScript脚本的几种方式,包括使用ScriptEngine、Nashorn和GraalVM,ScriptEngine适用... 目录Java脚本使用不同版本jdk的说明1.使用ScriptEngine执行javascript2.

Spring MVC如何设置响应

《SpringMVC如何设置响应》本文介绍了如何在Spring框架中设置响应,并通过不同的注解返回静态页面、HTML片段和JSON数据,此外,还讲解了如何设置响应的状态码和Header... 目录1. 返回静态页面1.1 Spring 默认扫描路径1.2 @RestController2. 返回 html2

Spring常见错误之Web嵌套对象校验失效解决办法

《Spring常见错误之Web嵌套对象校验失效解决办法》:本文主要介绍Spring常见错误之Web嵌套对象校验失效解决的相关资料,通过在Phone对象上添加@Valid注解,问题得以解决,需要的朋... 目录问题复现案例解析问题修正总结  问题复现当开发一个学籍管理系统时,我们会提供了一个 API 接口去

Java操作ElasticSearch的实例详解

《Java操作ElasticSearch的实例详解》Elasticsearch是一个分布式的搜索和分析引擎,广泛用于全文搜索、日志分析等场景,本文将介绍如何在Java应用中使用Elastics... 目录简介环境准备1. 安装 Elasticsearch2. 添加依赖连接 Elasticsearch1. 创

Spring核心思想之浅谈IoC容器与依赖倒置(DI)

《Spring核心思想之浅谈IoC容器与依赖倒置(DI)》文章介绍了Spring的IoC和DI机制,以及MyBatis的动态代理,通过注解和反射,Spring能够自动管理对象的创建和依赖注入,而MyB... 目录一、控制反转 IoC二、依赖倒置 DI1. 详细概念2. Spring 中 DI 的实现原理三、