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 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.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 {


    //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的核心几个文件就讲的很清楚了,实际上很简单,就是两个都可以解决,那就是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

发布了25 篇原创文章 · 获赞 9 · 访问量 3059

猜你喜欢

转载自blog.csdn.net/qq_40053836/article/details/98970804