• Java - SpringBoot整合Shiro(附源码地址)


    一. Shiro 简介

    ShiroApache下的一个安全框架。主要用于认证、授权、加密、会话管理等功能。这是摘自官网的一个截图:
    在这里插入图片描述
    针对授权,说明一点:Shiro 不会去维护用户以及维护权限。因此这些需要我们自己去设计和提供。通过相应的接口注入给 Shiro 即可。

    1.1 Shiro 架构

    这里同样摘自官网的一个架构图:
    在这里插入图片描述

    其中,ApplicationCode指的是我们的应用程序。也就是说,Shiro 架构中,有三个主要的组成部分:

    • Subject:主体,代表着当前的用户信息。 从图中可以发现,它也是直接和我们程序进行交互的一个对象。
    • SecurityManagerShiro的一个核心所有的交互都通过它来控制。例如:管理所有的Subject,负责认证、授权、会话管理。
    • Realm:可以当做一个元数据,当Shiro需要和一些安全相关的数据打交道的时候,就需要通过Realm来获得相关的信息。

    上文说到过我们自己去设计和提供用户的权限并注入给Shiro。而Realm本质上是一个特定于安全的DAO。封装了一些底层的数据操作。


    然后我们再看看SecurityManager里面又有哪些东西:
    在这里插入图片描述

    • SessionManager:会话管理器,用于管理Session的。
    • SessionDAO:封装底层操作API,比如我们可以通过它把Session存储到数据库。
    • CacheManager:缓存控制器,管理用户、权限的缓存。
    • Authentication身份认证
    • Authorization:用于权限验证,判断某个用户是否有某个操作的权限。

    二. SpringBoot整合Shiro + JWT

    2.1 设计思路

    先来说下大概的架构设计,由于这个项目是前后端分离的一个微服务项目, 因此需要采取无状态登录。 那么我们就不能使用Shiro来做登录的一个拦截,原因如下:

    • 首先Shiro这个框架,他是可以进行登录状态的一个拦截的。但是它的一个校验机制是基于Session来实现的。
    • Shiro默认的拦截器都是跳转URL页面,前后端分离之后,后端就无法干涉前端了。

    那么无状态怎么实现?就应该使用JWT来完成,而且对于微服务而言,JWT也是一个很好地选择。

    1. 登录完成后,服务器生成一个Token,返回给前端,前端自己写入到Cookie里面也好,本地也好。
    2. 后续的请求都自己带上这个Token即可正常访问。即无状态的登录。

    那么对于ShiroJWT的整合,它们的角色也就可以定下来了:

    • Shiro:用于权限的校验。比如管理员权限、游客权限呀等等。同时支持对Token的封装(AuthenticationToken)。
    • JWT:用于无状态登录。

    那么,怎么把两者整合起来呢?

    1. 我们知道,JWT的使用中,有一个拦截器或者一个过滤器。就是用来判断你的请求是否包含了对应的Token
    2. 那么我们可以在Shiro的配置中,加入JWT的拦截器。如果没带Token,就直接抛异常,提示必须携带Token
    3. 那么如果带了Token了,是不是就应该校验Token的有效性啦?这一步就可以交给ShiroRealm来完成校验或者是授权。

    那么接下来就可以从这些角度来开始编写代码了,大家可以在这个项目的基础上做一个修改:Java - SpringBoot整合JWT

    然后添加pom依赖:

    <dependency>
       <groupId>org.apache.shirogroupId>
        <artifactId>shiro-springartifactId>
        <version>1.9.0version>
    dependency>
    
    • 1
    • 2
    • 3
    • 4
    • 5

    2.2 Shiro 配置类编写

    我们可以写一个配置类ShiroConfig

    @Configuration
    public class ShiroConfig {
    
    }
    
    • 1
    • 2
    • 3
    • 4

    有哪些内容需要写呢?

    • SecurityManager:它是Shiro的核心啊,少了它的配置,咋玩Shiro。这里面配置对应的Realm。同时我们还需要注意关闭Session。因为我们要搞无状态。
    • ShiroFilterFactoryBeanShiro过滤器的一些配置,这里可以塞入我们的JWT过滤器。以及哪些路径不需要校验。

    备注:后面会一次性贴完代码。

    2.2.1 Shiro核心 - SecurityManager编写

    因为SecurityManager里面需要配置我们自定义的Realm,这里我先写一个没有任何实现的JwtRealm

    public class JwtRealm extends AuthenticatingRealm {
        @Override
        protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
            return null;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    紧接着回到我们的ShiroConfig,我们添加如下配置:

    1. 添加自定义Realm
    2. 关闭ShiroDao功能。
    3. 我们还应该禁止使用getSession功能。我们就应该自定义一个SubjectFactory,不创建Session功能。

    1.首先自定义JwtDefaultSubjectFactory

    public class JwtDefaultSubjectFactory extends DefaultWebSubjectFactory {
    
        @Override
        public Subject createSubject(SubjectContext context) {
            // 不创建 session
            context.setSessionCreationEnabled(false);
            return super.createSubject(context);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    2.实现上面三种效果:

    /**
     * 声明不要使用默认的 DefaultWebSubjectFactory 创建对象,因为我们不希望使用Session相关的功能
     */
    @Bean
    public SubjectFactory subjectFactory() {
        return new JwtDefaultSubjectFactory();
    }
    
    /**
     * 自定义的Shiro Realm
     */
    @Bean
    public Realm realm() {
        return new JwtRealm();
    }
    
    /**
     * Shiro 核心ecurityManager的配置
     */
    @Bean
    public DefaultWebSecurityManager securityManager() {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        // 设置我们自定义的JwtRealm
        securityManager.setRealm(realm());
        // 关闭ShiroDao、Session功能
        DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
        DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator();
        // 不需要将 Shiro Session 中的东西存到任何地方
        defaultSessionStorageEvaluator.setSessionStorageEnabled(false);
        subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator);
        securityManager.setSubjectDAO(subjectDAO);
        // 不能使用Subject的getSession函数
        securityManager.setSubjectFactory(subjectFactory());
        return securityManager;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35

    2.2.2 Shiro过滤器配置

    我们先自定义一个Jwt的过滤器:JwtFilter:具体实现后面说

    public class JwtFilter extends AccessControlFilter {
    
        @Override
        protected boolean isAccessAllowed(ServletRequest servletRequest, ServletResponse servletResponse, Object o) throws Exception {
            return false;
        }
    
        @Override
        protected boolean onAccessDenied(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception {
            return false;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    Shiro拦截器配置:

    /**
     * Shiro 拦截器配置
     */
    @Bean
    public ShiroFilterFactoryBean shiroFilterFactoryBean() {
        ShiroFilterFactoryBean shiroFilter = new ShiroFilterFactoryBean();
        shiroFilter.setSecurityManager(securityManager());
        shiroFilter.setLoginUrl("/login");
        // 拦截器设置
        Map<String, Filter> filterMap = new HashMap<>();
        // jwt的相关过滤器
        filterMap.put("jwt", new JwtFilter());
        // 这两个是默认的实现,可以不填,但是可以结合下面的来看,anno代表无需认证即可访问
        filterMap.put("anon", new AnonymousFilter());
        // 登出也是无需进入拦截器
        filterMap.put("logout", new LogoutFilter());
        shiroFilter.setFilters(filterMap);
        // 配置相关的连接和对应的拦截器,按顺序执行
        Map<String, String> filterRuleMap = new LinkedHashMap<>();
        // 登录不走拦截,登出走登出的默认实现。其余所有请求都走jwt对应设置的过滤器。
        filterRuleMap.put("/login", "anon");
        filterRuleMap.put("/logout", "logout");
        filterRuleMap.put("/**", "jwt");
        shiroFilter.setFilterChainDefinitionMap(filterRuleMap);
    
        return shiroFilter;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27

    2.2.3 其余配置(支持注解)

    我们的项目里面需要用到AOP,因为需要用到RequiresRoles相关的注解,用来做Shiro的权限校验。

    /**
     * 下面的代码是添加注解支持
     */
    @Bean
    @DependsOn({"lifecycleBeanPostProcessor"})
    public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
        // 设置代理类
        DefaultAdvisorAutoProxyCreator creator = new DefaultAdvisorAutoProxyCreator();
        creator.setProxyTargetClass(true);
        return creator;
    }
    
    /**
     * 开启aop注解支持,这样就可以用RequiresRoles、RequiresPermissions等注解
     *
     * @param securityManager
     * @return
     */
    @Bean("authorizationAttributeSourceAdvisor")
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(DefaultWebSecurityManager securityManager) {
        AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
        authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
        return authorizationAttributeSourceAdvisor;
    }
    
    // Shiro生命周期处理器,让Shiro管理对应的Bean
    @Bean
    public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
        return new LifecycleBeanPostProcessor();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30

    2.2.4 最终ShiroConfig配置

    import com.pro.config.jwt.JwtDefaultSubjectFactory;
    import com.pro.config.jwt.JwtFilter;
    import com.pro.config.jwt.JwtRealm;
    import org.apache.shiro.mgt.DefaultSessionStorageEvaluator;
    import org.apache.shiro.mgt.DefaultSubjectDAO;
    import org.apache.shiro.mgt.SubjectFactory;
    import org.apache.shiro.realm.Realm;
    import org.apache.shiro.spring.LifecycleBeanPostProcessor;
    import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
    import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
    import org.apache.shiro.web.filter.authc.AnonymousFilter;
    import org.apache.shiro.web.filter.authc.LogoutFilter;
    import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
    import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.context.annotation.DependsOn;
    
    import javax.servlet.Filter;
    import java.util.HashMap;
    import java.util.LinkedHashMap;
    import java.util.Map;
    
    /**
     * @author Zong0915
     * @date 2022/11/11 下午7:33
     */
    @Configuration
    public class ShiroConfig {
        /**
         * 声明不要使用默认的 DefaultWebSubjectFactory 创建对象,因为我们不希望使用Session相关的功能
         */
        @Bean
        public SubjectFactory subjectFactory() {
            return new JwtDefaultSubjectFactory();
        }
    
        /**
         */
        @Bean
        public Realm realm() {
            return new JwtRealm();
        }
    
        /**
         * Shiro 核心ecurityManager的配置
         */
        @Bean
        public DefaultWebSecurityManager securityManager() {
            DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
            // 设置我们自定义的JwtRealm
            securityManager.setRealm(realm());
            // 关闭ShiroDao、Session功能
            DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
            DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator();
            // 不需要将 Shiro Session 中的东西存到任何地方
            defaultSessionStorageEvaluator.setSessionStorageEnabled(false);
            subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator);
            securityManager.setSubjectDAO(subjectDAO);
            // 不能使用Subject的getSession函数
            securityManager.setSubjectFactory(subjectFactory());
            return securityManager;
        }
    
        /**
         * Shiro 拦截器配置
         */
        @Bean
        public ShiroFilterFactoryBean shiroFilterFactoryBean() {
            ShiroFilterFactoryBean shiroFilter = new ShiroFilterFactoryBean();
            shiroFilter.setSecurityManager(securityManager());
            shiroFilter.setLoginUrl("/login");
            // 拦截器设置
            Map<String, Filter> filterMap = new HashMap<>();
            // jwt的相关过滤器
            filterMap.put("jwt", new JwtFilter());
            // 这两个是默认的实现,可以不填,但是可以结合下面的来看,anno代表无需认证即可访问
            filterMap.put("anon", new AnonymousFilter());
            // 登出也是无需进入拦截器
            filterMap.put("logout", new LogoutFilter());
            shiroFilter.setFilters(filterMap);
            // 配置相关的连接和对应的拦截器,按顺序执行
            Map<String, String> filterRuleMap = new LinkedHashMap<>();
            // 登录不走拦截,登出走登出的默认实现。其余所有请求都走jwt对应设置的过滤器。
            filterRuleMap.put("/login", "anon");
            filterRuleMap.put("/logout", "logout");
            filterRuleMap.put("/**", "jwt");
            shiroFilter.setFilterChainDefinitionMap(filterRuleMap);
    
            return shiroFilter;
        }
    
        /**
         * 下面的代码是添加注解支持
         */
        @Bean
        @DependsOn({"lifecycleBeanPostProcessor"})
        public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
            // 设置代理类
            DefaultAdvisorAutoProxyCreator creator = new DefaultAdvisorAutoProxyCreator();
            creator.setProxyTargetClass(true);
            return creator;
        }
    
        /**
         * 开启aop注解支持,这样就可以用 RequiresRoles、RequiresPermissions等注解
         *
         * @param securityManager
         * @return
         */
        @Bean("authorizationAttributeSourceAdvisor")
        public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(DefaultWebSecurityManager securityManager) {
            AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
            authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
            return authorizationAttributeSourceAdvisor;
        }
    
        // Shiro生命周期处理器
        @Bean
        public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
            return new LifecycleBeanPostProcessor();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122
    • 123

    2.3 JwtFilter 具体实现

    我们项目里面原本还有一个拦截器TokenInterceptor,那么我们这里整合了Shiro了,就不再使用它了,可以把它删除。改为JwtFilterJwtFilter里面我们要做什么事情?继承AccessControlFilter,那么一般实现两个接口:

    • isAccessAllowed调用链中,会先执行这个函数,如果返回true,代表允许访问。如果返回false,则进入下面的onAccessDenied函数。我们可以写一些自定义的注解,让某些操作跳过这个Shiro校验,不需要登录。
    • onAccessDenied:返回false代表不允许访问。反之可以访问。这里面一般用来判断Token的合法性,然后进行登录。
    import lombok.extern.slf4j.Slf4j;
    import org.apache.shiro.web.filter.AccessControlFilter;
    import javax.servlet.ServletRequest;
    import javax.servlet.ServletResponse;
    import javax.servlet.http.HttpServletRequest;
    
    /**
     * @author Zong0915
     * @date 2022/11/11 下午8:34
     */
    @Slf4j
    public class JwtFilter extends AccessControlFilter {
    
        /**
         * 返回false,则进入onAccessDenied函数处理
         * 返回true,则该请求被允许访问。
         * 一般这里可以判断下是否携带Token,如果没携带,直接返回false即可
         */
        @Override
        protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object o) throws Exception {
            // 返回false,走onAccessDenied()
            return false;
        }
    
        /**
         * 返回true代表登录通过,这里可以用来校验JWT Token的合法性
         */
        @Override
        protected boolean onAccessDenied(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception {
            HttpServletRequest request = (HttpServletRequest) servletRequest;
            String jwt = request.getHeader("token");
            // 封装一下AuthenticationToken
            JwtToken jwtToken = new JwtToken(jwt);
            log.info("获取到请求头中的token:{}", jwt);
            try {
                // 委托 realm 进行登录认证 ,一种固定写法,这里调用的就是我们自定义的JwtRealm的实现
                // 这里登录成功之后,后面就可以通过SecurityUtils.getSubject()来获取对应的Subject了
                // Subject就包含了相关的JWTToken信息。
                getSubject(servletRequest, servletResponse).login(jwtToken);
            } catch (Exception e) {
                e.printStackTrace();
                //调用下面的方法向客户端返回错误信息
                return false;
            }
            return true;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47

    我的理解是:

    1. JwtFilter中,对Token进行了拦截。
    2. 调用getSubject(servletRequest, servletResponse).login(jwtToken);函数进行登录。
    3. login这个函数呢,相当于把这次校验的任务,委派给Realm来执行,而我们之前ShiroConfig中配置了自定义的JwtRealm,因此实际上它会调用JwtRealm中重写的方法。
    4. 如果登录成功了,那么本次请求中的任意一个地方,都可以根据SecurityUtils.getSubject();拿到我们的登录信息。

    备注:

    getSubject(servletRequest, servletResponse).login(jwtToken);
    //  getSubject 的源码:
    protected Subject getSubject(ServletRequest request, ServletResponse response) {
       return SecurityUtils.getSubject();
    }
    // 等同于
    SecurityUtils.getSubject().login(jwtToken)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    2.3.1 自定义JwtToken封装

    JwtToken需要实现AuthenticationToken接口,主要就是把Principal、Credentials的值都和Token绑定。

    public class JwtToken implements AuthenticationToken {
    
        private String jwt;
    
        public JwtToken(String jwt) {
            this.jwt = jwt;
        }
    
        // 类似是用户名
        @Override
        public Object getPrincipal() {
            return jwt;
        }
    
        // 类似密码
        @Override
        public Object getCredentials() {
            return jwt;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    2.4 JwtRealm 具体实现

    自定义JwtRealm需要继承AuthenticatingRealm,我们需要注意以下几点:

    1. 重写doGetAuthenticationInfo函数,这个函数是我们进行校验的一个核心实现。
    2. 因为我们自定义实现了JwtToken,因此我们需要重写supports函数,让其支持我们自定义的JwtToken。否则会报错。如图:
      在这里插入图片描述

    最终代码如下:

    @Slf4j
    public class JwtRealm extends AuthenticatingRealm {
        @Autowired
        private JwtUtil jwtUtil;
    
        /*
         * 多重写一个support
         * 标识这个Realm是专门用来验证JwtToken
         * 不负责验证其他的token(UsernamePasswordToken)
         * 必须重写此方法,不然Shiro会报错
         * */
        @Override
        public boolean supports(AuthenticationToken token) {
            //这个token就是从过滤器中传入的jwtToken
            return token instanceof JwtToken;
        }
    
        @Override
        protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
            String jwt = (String) token.getPrincipal();
            if (jwt == null) {
                throw new SignatureException("Token不能为空!");
            }
            // 判断
            Long userId = jwtUtil.getSubject(jwt);
            TokenContext.setUserId(userId);
    
            return new SimpleAuthenticationInfo(jwt, jwt, "JwtRealm");
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30

    2.5 其他优化

    1. 原本的项目中,通过的是ThreadLocal来存储了一个userId。也就是TokenContext。但是现在整合了Shiro,可以直接通过SecurityUtils.getSubject()来获取对应的属性了。
    2. 我们的pom依赖中,和JWT相关的有两个依赖:jjwtjava-jwt。两个差不多,但是jjwt这个依赖已经停止维护很久了,建议使用java-jwt

    2.5.1 使用java-jwt进行JWT编程

    1.创建一个SpringBeanUtil类,用来获取Bean的:

    import lombok.extern.slf4j.Slf4j;
    import org.springframework.beans.BeansException;
    import org.springframework.context.ApplicationContext;
    import org.springframework.context.ApplicationContextAware;
    import org.springframework.stereotype.Component;
    
    @Component
    @Slf4j
    public class SpringBeanUtil implements ApplicationContextAware {
    
        private static ApplicationContext applicationContext;
    
        @Override
        public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
            if (SpringBeanUtil.applicationContext == null) {
                SpringBeanUtil.applicationContext = applicationContext;
            }
            log.info("\r\n----------加载applicationContext成功-----------------");
        }
    
        // 获取applicationContext
        public static ApplicationContext getApplicationContext() {
            return applicationContext;
        }
    
        // 通过class获取Bean.
        public static <T> T getBean(Class<T> clazz) {
            try {
                char[] cs = clazz.getSimpleName().toCharArray();
                cs[0] += 32;// 首字母大写到小写
                return (T) getApplicationContext().getBean(String.valueOf(cs));
            } catch (Exception e) {
                e.printStackTrace();
                return null;
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37

    2.将配置文件配置类和JwtUtil分离开,创建一个AppConfig

    @Component
    @ConfigurationProperties(prefix = "config.jwt")
    public class AppConfig {
        private String secret;
        private int expire;
    
        public static String GetSecret() {
            AppConfig configBean = SpringBeanUtil.getBean(AppConfig.class);
            String secretKey = configBean.getSecret();
            return secretKey == null ? StringUtils.EMPTY : secretKey;
        }
    
        public static int GetExpire() {
            AppConfig configBean = SpringBeanUtil.getBean(AppConfig.class);
            int expire = configBean.getExpire();
            return expire;
        }
    
        public String getSecret() {
            return secret;
        }
    
        public void setSecret(String secret) {
            this.secret = secret;
        }
    
        public void setExpire(int expire) {
            this.expire = expire;
        }
    
        public int getExpire() {
            return expire;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34

    3.创建常量类JwtConstant

    public class JwtConstant {
        public static final String USER_ID = "userId";
    }
    
    • 1
    • 2
    • 3

    4.核心实现,Java-jwt版本的JWT实现JwtUtil

    import com.auth0.jwt.JWT;
    import com.auth0.jwt.JWTVerifier;
    import com.auth0.jwt.algorithms.Algorithm;
    import com.pro.constant.JwtConstant;
    import io.jsonwebtoken.*;
    import org.apache.shiro.SecurityUtils;
    import org.apache.shiro.subject.Subject;
    import org.apache.tomcat.util.codec.binary.Base64;
    import java.util.Calendar;
    import java.util.Map;
    import java.util.UUID;
    
    /**
     * @author Zong0915
     * @date 2022/11/11 下午7:16
     */
    public class JwtUtil {
        private static final SignatureAlgorithm ALGORITHM = SignatureAlgorithm.HS256;
    
        // 生成Jwt Token
        public static String buildToken(Long userId, Map<String, Object> claims) {
            Calendar expires = Calendar.getInstance();
            JwtBuilder jwtBuilder = Jwts
                    .builder()
                    .setClaims(claims)
                    // JWT唯一标识
                    .setId(UUID.randomUUID().toString())
                    // 签发时间
                    .setIssuedAt(expires.getTime())
                    // Subject主体,存我们的userId
                    .setSubject(userId + "")
                    // 签名算法和对应的秘钥
                    .signWith(ALGORITHM, AppConfig.GetSecret());
    
            // 设置过期时间
            expires.add(Calendar.SECOND, AppConfig.GetExpire());
            jwtBuilder.setExpiration(expires.getTime());
            // 生成Token
            return jwtBuilder.compact();
        }
    
        public static Long getUserId() {
            Subject subject = SecurityUtils.getSubject();
            if (subject == null) {
                return null;
            }
            // 如果校验成功,即getSubject(servletRequest, servletResponse).login(jwtToken);调用成功
            // 那么这里就可以拿到对应的Token,因为我们重写了JwtToken,它的Principals值就是我们的Token
            String token = (String) subject.getPrincipals().getPrimaryPrincipal();
            return getUerIdFromClaim(token);
        }
    
        // 根据Token 拿到我们的userId
        public static Long getUerIdFromClaim(String token) {
            Object o = Jwts
                    .parser()
                    .setSigningKey(AppConfig.GetSecret())
                    .parseClaimsJws(token)
                    .getBody()
                    .get(JwtConstant.USER_ID);
            if (o != null) {
                return Long.parseLong(o + "");
            }
            return null;
        }
    
        public static boolean isVerify(String jwtToken) {
            // 这里一定要经过Base64解码
            Algorithm algorithm = Algorithm.HMAC256(Base64.decodeBase64(AppConfig.GetSecret()));
            JWTVerifier verifier = JWT.require(algorithm).build();
            verifier.verify(jwtToken);  // 校验不通过会抛出异常
            return true;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74

    5.JwtFilter修改(因为还是用的老一套的JWT,不再使用注入了,直接静态函数调用)

    import com.alibaba.fastjson.JSON;
    import lombok.extern.slf4j.Slf4j;
    import org.apache.shiro.web.filter.AccessControlFilter;
    
    import javax.servlet.ServletRequest;
    import javax.servlet.ServletResponse;
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    import java.io.IOException;
    
    /**
     * @author Zong0915
     * @date 2022/11/11 下午8:34
     */
    @Slf4j
    public class JwtFilter extends AccessControlFilter {
    
        /**
         * 判断是否携带了有效的JwtToken
         */
        @Override
        protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object o) throws Exception {
            //
            return false;
        }
    
        /**
         * 返回true代表登录通过,这里可以用来校验JWT Token的合法性
         */
        @Override
        protected boolean onAccessDenied(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception {
            HttpServletRequest request = (HttpServletRequest) servletRequest;
            String jwt = request.getHeader("token");
            JwtToken jwtToken = new JwtToken(jwt);
            log.info("获取到请求头中的token:{}", jwt);
            try {
                // 委托 realm 进行登录认证 ,一种固定写法,这里调用的就是我们自定义的JwtRealm的实现
                // 这里登录成功之后,后面就可以通过SecurityUtils.getSubject()来获取对应的Subject了
                // Subject就包含了相关的JWTToken信息。
                getSubject(servletRequest, servletResponse).login(jwtToken);
            } catch (Exception e) {
                onLoginFail(servletResponse, e);
                log.error(e.getMessage());
                //调用下面的方法向客户端返回错误信息
                return false;
            }
            return true;
        }
    
        private void onLoginFail(ServletResponse response, Exception e) throws IOException {
            HttpServletResponse httpResponse = (HttpServletResponse) response;
            httpResponse.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
            // 这里你自己自定义一个返回报文就好了,这里单纯的就是展示报错信息
            httpResponse.getWriter().write(JSON.toJSONString(e.getMessage()));
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56

    6.JwtRealm修改(同理):

    import com.pro.config.JwtUtil;
    import io.jsonwebtoken.SignatureException;
    import lombok.extern.slf4j.Slf4j;
    import org.apache.shiro.authc.AuthenticationException;
    import org.apache.shiro.authc.AuthenticationInfo;
    import org.apache.shiro.authc.AuthenticationToken;
    import org.apache.shiro.authc.SimpleAuthenticationInfo;
    import org.apache.shiro.realm.AuthenticatingRealm;
    
    /**
     * @author Zong0915
     * 自定义的Shiro Realm
     * @date 2022/11/11 下午8:14
     */
    @Slf4j
    public class JwtRealm extends AuthenticatingRealm {
    
        /*
         * 多重写一个support
         * 标识这个Realm是专门用来验证JwtToken
         * 不负责验证其他的token(UsernamePasswordToken)
         * 必须重写此方法,不然Shiro会报错
         * */
        @Override
        public boolean supports(AuthenticationToken token) {
            //这个token就是从过滤器中传入的jwtToken
            return token instanceof JwtToken;
        }
    
        @Override
        protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
            String jwt = (String) token.getPrincipal();
            if (jwt == null) {
                throw new SignatureException("Token不能为空!");
            }
            // 校验JWT,如果不通过的话,就会抛出异常,然后被JwtFilter捕捉
            JwtUtil.isVerify(jwt);
            return new SimpleAuthenticationInfo(jwt, jwt, "JwtRealm");
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40

    7.Controller修改:

    import com.pro.config.JwtUtil;
    import com.pro.constant.JwtConstant;
    import org.apache.shiro.SecurityUtils;
    import org.apache.shiro.subject.Subject;
    import org.springframework.web.bind.annotation.PostMapping;
    import org.springframework.web.bind.annotation.RequestParam;
    import org.springframework.web.bind.annotation.RestController;
    
    import java.util.HashMap;
    import java.util.Map;
    
    @RestController
    public class UserController {
        @PostMapping("/login")
        public String login(@RequestParam("userId") Long userId) {
            Map<String, Object> chaim = new HashMap<>();
            chaim.put(JwtConstant.USER_ID, userId);
            String token = JwtUtil.buildToken(userId, chaim);
    
            return "Success, Jwt Token : " + token;
        }
    
        @PostMapping("/getUser")
        public String getUser() {
            Long userId = JwtUtil.getUserId();
            Subject currentUser = SecurityUtils.getSubject();
            if (userId != null) {
                return "成功拿到用户信息: " + currentUser.getPrincipal();
            }
            return "用户信息为空";
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32

    2.6 源码地址及测试

    1.登录操作:
    在这里插入图片描述

    2.携带Token访问:
    在这里插入图片描述

    3.不携带Token访问:
    在这里插入图片描述

    完整项目地址

    三. 总结

    先说下一个调用链:

    3.1 登录流程:

    1. 一般是数据库校验登录,通过之后,我们就可以在服务器端生成JWT了。
    2. 但是注意,不要放敏感信息到Token里面,因为默认Base64编码。
    3. 最后会把这个Token返回给客户端。客户端后续只需要每次请求都携带这个Token即可。

    核心操作:
    在这里插入图片描述

    3.2 核心配置

    第一点:我们需要使用JWT来实现无状态的登录。即携带Token访问接口。而不是SessionSession存储于服务器,Shiro默认是开启Session功能的,因此我们要把它关闭。

    • 声明一个JwtDefaultSubjectFactory替代默认的DefaultWebSubjectFactory
    • 关闭ShiroDao以及禁用Session

    第二点:配置自定义的Realm和过滤器。

    • ShiroFilterFactoryBean:配置拦截器、哪些路径不需要拦截、哪些需要拦截(配置自定义的JwtFilter
    • 配置Realm
      在这里插入图片描述

    第三点:开启注解支持(详细的看ShiroConfig代码)

    3.3 校验Token流程

    携带Token访问流程:

    1. 请求被JwtFilter拦截。调用getSubject(servletRequest, servletResponse).login(jwtToken);委派JwtRealm来完成登录校验。
    2. JwtRealm中调用JWT相关API完成校验。JwtUtil.isVerify(jwt);
    3. 不通过,默认抛出异常,通过则正常访问接口。

    JwtRealm需要注意的三点:

    • 重写supports,需要支持我们自定义的Token:JwtToken。一定要重写。
    • 如果需要授权:重写doGetAuthorizationInfo()函数,一般这里需要读数据库中我们定义的权限,然后将权限注入,这样结合@RequiresRoles注解,就完成了权限的控制。 不在本篇文章的编码范围内。
    • 如果需要认证:重写doGetAuthenticationInfo()函数。主要是调用JWT相关的API

    3.4 待更新内容

    最后,本文其实还留下几个功能点没有开发:

    • Realm中的权限验证(比如AdminUser这种权限的校验),还需要配合注解来完成,这里还涉及到查表。因为需要我们自己去维护权限数据。
    • 我们还可以在isAccessAllowed()函数中,增加一个注解校验,跳过权限校验。
    • 同时,用户权限这块是否可以加一个缓存呢?毕竟Shiro框架就自带一个缓存功能。

    上面的内容会继续更新~

  • 相关阅读:
    C++ 之多态总结
    使用wireshark抓取Tcp三次握手
    【Linux】C语言之IP地址转换方法
    Redis持久化-RDB和AOF
    阿里三面惨虐,spring,jvm,mybatis,并发编程等一窍不通
    Ansible的脚本 --- playbook 剧本
    如何选购过孔滑环
    Vue实现todolist的删除功能
    vue3学习实战
    【树莓派】起步(Snappy Ubuntu Core)
  • 原文地址:https://blog.csdn.net/Zong_0915/article/details/127809705