Springboot+Shiro+Jwt实现权限控制

2023-11-05

前置背景

  • 为什么写下这篇文章?

因为需要实现一个设备管理系统的权限管理模块,在查阅很多博客以及其他网上资料之后,发现重复、无用的博客很多,因此写一篇文章来记录,以便后面复习。

  1. 涉及的知识点主要有下列知识点:

    1. JWT
    2. shiro
  2. 书写顺序

    1. 首先使用springboot 结合 jwt完成前后端分离的token认证。
    2. 其次结合shiro完成shiro+jwt的前后端分离的权限认证管理。

权限管理的表结构设计

  1. 一个user可以拥有多个role,一个role也可以被多个user拥有, 一个 角色拥有多个权限即功能,一个权限可以被多个role拥有。

用户、角色、权限类

 表结构图

Part1: spring boot + jwt

这一部分就可以完成前后端分离项目的登录功能。在不需要添加权限管理的情况下,就可以满足需求。

Spring boot集成JWT

<dependency>
  <groupId>com.auth0</groupId>
  <artifactId>java-jwt</artifactId>
  <version>3.8.2</version>
</dependency>
复制代码

思路整理

  1. 为什么需要使用到jwt?
  2. jwt是什么
  3. Java如何使用jwt

在前后端分离的项目中,由服务器使用的会话管理session无法满足需求。需要一种技术做会话管理。因此选择JWT。 Json web token (JWT) : 是目前流行的跨域认证解决方案,是一种基于 Token 的认证授权机制。 JWT的数据结构分为三部分 header payload signature。 这三部分通过.连接,如下

Token示例:

    eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.
    eyJwYXNzd29yZCI6InN1cGVyIiwiZXhwIjoxNjYzMTIzNDgzLCJ1c2VybmFtZSI6InN1cGVyIn0.
    5xVg6IuOLe_uVwwOaeyRDbTHRjfmIbNsnb-DP9-Ic20
复制代码

如何使用:当用户登录系统后,服务端给前端发送一个基于用户信息创建的token,然后在此后的每一次前端请求都会携带token。服务端通过拦截器拦截请求,同时验证携带的token是否正确。如果正确则放行请求,不正确则拒绝通过。 思路流程图:

token的创建和验证

JWTUtil.java负责创建和验证jwt格式的token

    public class JWTUtil {
        private static final long EXPIRE_TIME = 3 * 60 * 1000;//默认3分钟
        //私钥
        private static final String TOKEN_SECRET = "privateKey";
    ​
        public static String createToken(UserEntity userModel) {
            try {
            // 设置过期时间
            Date date = new Date(System.currentTimeMillis() + EXPIRE_TIME);
            log.info(String.valueOf(date));
            // 私钥和加密算法
            Algorithm algorithm = Algorithm.HMAC256(TOKEN_SECRET);
            // 设置头部信息
            Map<String, Object> header = new HashMap<>(2);
            header.put("Type", "Jwt");
            header.put("alg", "HSA256");
            // 返回token字符串
            return JWT.create()
                .withHeader(header)
                .withClaim("username", userModel.getUsername())
                .withExpiresAt(date)
                .sign(algorithm);
            } catch (Exception e) {
                e.printStackTrace();
                return null;
            }
        }
        
        /**
        * 检验token是否正确
        *
        * @param **token**
        * @return
        */
        public static boolean verifyToken(String token, String username) {
            log.info("验证token..");
            try {
                Algorithm algorithm = Algorithm.HMAC256(TOKEN_SECRET);
                JWTVerifier verifier = JWT.require(algorithm)
                                          .withClaim("username",username).build();
                // 验证不通过会抛出异常。
                verifier.verify(token);
                return true;
            } catch (Exception e) {
                log.info("verifyToken = {}",e.getMessage());
                return false;
            }
        }
    ​
        // 通过withClaim添加在token里面的数据都可以通过这种方式获取
        public static String getUsername(String token){
            DecodedJWT jwt = JWT.decode(token);
            String username = String.valueOf(jwt.getClaim("username"));
            if (StringUtils.hasLength(username)){
                return username;
            }
            return  null;
        }
    }
    
复制代码

拦截器的创建和配置

  1. 创建拦截器,拦截请求
@Slf4j
@Component
public class LoginInterceptor implements HandlerInterceptor  {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 当前端是通过在请求里面以 token="xxxx.xxx.zzz"的方式传递时,通过getHeader("token")
        // 的方式获取。
        String token = request.getHeader("token");
        log.info("token = {}",token);
        if (token == null){
            setReturnInfo((HttpServletResponse) response,401,"请携带token");
            return false;
        }
        // 解析token中的数据,JWTUtil.getUsername();
        // 在这里可以通过findUserByUsername的方式从数据源中获取数据
        
        
        // 假定登录用户是super, 并传递给此方法传递参数 
        if ( !JWTUtil.verifyToken(token,"super")){
            setReturnInfo((HttpServletResponse) response,401,"token已过期");
            return false;
        }
        return true;
    }
    
    private static void setReturnInfo(HttpServletResponse httpResponse,int status,String msg) throws IOException {
        log.info("token = null");
        httpResponse.setHeader("Access-Control-Allow-Credentials", "true");
        httpResponse.setHeader("Access-Control-Allow-Origin", "*");
        httpResponse.setCharacterEncoding("UTF-8");
        response.setContentType("application/json;charset=utf-8");
        Map<String,String> result  =new HashMap<>();
        result.put("status",String.valueOf(status));
        result.put("msg",msg);
        httpResponse.getWriter().print(JSONUtils.toJSONString(result));
        
        // 前端可根据返回的status判断

    }
    
}
复制代码

2. 配置拦截器

InterceptorConfig.java负责将使用了JwtUtil的拦截器配置进入Spring boot。

    @Configuration
    public class InterceptorConfig  implements WebMvcConfigurer {
        
        @Resource
        private LoginInterceptor interceptor;
    ​
        @Override
        public void addInterceptors(InterceptorRegistry registry) {
            List<String> patterns = new ArrayList<>();
    ​
            // 添加过滤路由 默认拦截所有请求
            patterns.add("/**");
            registry.addInterceptor(interceptor)
                    .addPathPatterns(patterns)
                    .excludePathPatterns("/user/login"); // 用户登录请求不拦截
            
        }
    ​
    }
    
复制代码

Part2:shiro+jwt

  1. 明确shiro是什么
  2. shiro的工作流程
  3. spring boot如何配置shiro框架
  4. shiro和jwt的整合

shiro是一个权限管理框架,相对于Spring Security而言,代码简单。适用场景多,不只局限于Java。 shiro的架构原理图:

Subject:当前和系统交互的"用户",可以是人,系统,第三方插件等。统称为Subject。 SecurityManager:类似于Spring容器的一个容器,是Shiro的核心。管理众多的组件。
Authenticator:认证组件,用户需要先通过系统认证,在进行用户授权,需要先判断系统中是否有这个用户,在进行后续操作,因此在这里就是进行系统认证的地方。
Authorizer:授权,当一个用户是属于当前系统时,这个用户的一些操作就需要判断是否有权力去做这件事情。在这里就需要进行授权相关的
Realm 可以理解为数据源,就是在realm记录了那些属于本系统的用户,他们具有什么样的角色

就相当于在一个拥有多个公司的工业园区,人们需要有这个园区的卡片,才允许进入园区,而进入园区之后需要由本公司的门禁你才能进入公司,否则就不能进入公司一样。A公司的员工不能进入B公司。 Subject 就是员工,或者快递小哥,SecurityManager 就是园区门禁系统,Authenticator 就是门禁的闸机 Authorizer 的作用就是公司的门禁一样,realm 就是园区系统的数据库,记录了系统的用户和权限信息 。

工作流程:一个subject通过login()方法,将subject的信息提交给SecurityManager,SecurityManager调用自己的组件去判断,认证,授权等。shiro是通过filter来进行拦截请求的,因此在结合jwt时,就不需要interceptor就能达到预期的效果。

思路流程图:

第一步改造JwtToken

    // AuthenticationToken 是shiro框架的。
    public class JWTToken implements AuthenticationToken {
        private String token;
    ​
        public JWTToken(String token) {
            this.token = token;
        }
    ​
        public String getToken() {
            return token;
        }
    ​
        @Override
        public Object getPrincipal() {
            return token;
        }
    ​
        @Override
        public Object getCredentials() {
            return token;
        }
    ​
    }
复制代码

JWTUtils代码不变,创建和验证逻辑如上。 第二步编写自己的数据源Realm

    public class MyRealm extends AuthorizingRealm {
    ​
        // 指定凭证匹配器。匹配器工作在认证后,授权前。
        public MyRealm() {
            this.setCredentialsMatcher(new JWTCredentialsMatcher());
        }
        @Resource
        UserServiceInt userServiceInt;
        // 判断token是否为JWTToken 必须重写
        @Override
        public boolean supports(AuthenticationToken token) {
            return token instanceof JWTToken;
        }
        // 认证
        @Override
        protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
    ​
            log.info("AuthenticationInfo 开始认证");
            String token = ((JWTToken) authenticationToken).getToken();
            String username = JWT.decode(token).getClaim("username").asString();
            // 从系统的数据库查找是否拥有这个用户,也可以提前把数据加载到Redis中,从redis中查找即可。
            UserModel user = userServiceInt.getUserByUsername(username);
            if (user == null) {
                log.info("user为空"); 
                // 认证不通过
                return null;
            }
            SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo(
                    user,
                    token,
                    "myRealm"
            );
            return simpleAuthenticationInfo;
        }
        // 授权
        @Override
        protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
            log.info("开始授权...");
            // 从PrincipalCollection获取user
            UserModel userModel = (UserModel) principalCollection.getPrimaryPrincipal();
    ​
            SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
            // 模拟数据库操作。实际上可以利用mybatis的级联查询,查询出用户的角色和权限信息。
            if (userModel.getUsername().equals("super")){
                // 添加用户角色
                simpleAuthorizationInfo.addRole("admin");
                // 添加用户权限
                simpleAuthorizationInfo.addStringPermission("user:list");
            }
            return simpleAuthorizationInfo;
        }
    }
复制代码
public class JWTCredentialsMatcher implements CredentialsMatcher {
   @Override
   public boolean doCredentialsMatch(AuthenticationToken authenticationToken, AuthenticationInfo authenticationInfo) {
​
       String token = ((JWTToken)authenticationToken).getToken();
       log.info("JWTCredentialsMatcher token = {}",token);
       UserModel userModel = (UserModel) authenticationInfo.getPrincipals().getPrimaryPrincipal();
       log.info("JWTCredentialsMatcher token = {}",userModel.toString());
       // 调用JwtUtils验证token即可
       return JWTUtil.verifyToken(token, userModel.getUsername(), userModel.getPassword());
   }
}
复制代码

第三:编写filter拦截前端请求

    public class JwtFilter extends BasicHttpAuthenticationFilter {

        @Override
        protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
            log.info("isAccessAllowed : 验证是否拥有token");
            String token = ((HttpServletRequest) request).getHeader("token");
            HttpServletResponse servletResponse = (HttpServletResponse) response;
            if (!StringUtils.hasLength(token)) {
                try {
                    setReturnInfo(servletResponse, 401, "token为空");
                } catch (IOException e) {
                    e.printStackTrace();
                    return false;
                }
            }
            return executeLogin(request,response);
        }

        @Override
        protected boolean executeLogin(ServletRequest request, ServletResponse response)   {
            log.info("executeLogin : 执行登录");
            HttpServletRequest httpServletRequest = (HttpServletRequest) request;
            HttpServletResponse httpServletResponse = (HttpServletResponse) response;
            String token = httpServletRequest.getHeader("token");

            JWTToken jwtToken = new JWTToken(token);
            // 提交给realm进行登入,如果错误他会抛出异常并被捕获
            try {
                getSubject(request, response).login(jwtToken);
            } catch (Exception e) {
                log.info("认证出现异常:{}", e.getMessage());
                try {
                    setReturnInfo(httpServletResponse,401,"token错误");
                } catch (IOException ex) {
                    ex.printStackTrace();
                }
                return false;
            }
            // 如果没有抛出异常则代表登入成功,返回true
            return true;
        }

        @Override
        protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
            log.info("登录失败");
            return super.onAccessDenied(request, response);
        }

        /**
         * 对跨域提供支持
         */
        @Override
        protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {
            HttpServletRequest httpServletRequest = (HttpServletRequest) request;
            HttpServletResponse httpServletResponse = (HttpServletResponse) response;
            httpServletResponse.setHeader("Access-control-Allow-Origin", httpServletRequest.getHeader("Origin"));
            httpServletResponse.setHeader("Access-Control-Allow-Methods", "GET,POST,OPTIONS,PUT,DELETE");
            httpServletResponse.setHeader("Access-Control-Allow-Headers", httpServletRequest.getHeader("Access-Control-Request-Headers"));
            httpServletResponse.setCharacterEncoding("UTF-8");
            // 跨域时会首先发送一个option请求,这里我们给option请求直接返回正常状态
            if (httpServletRequest.getMethod().equals(RequestMethod.OPTIONS.name())) {
                httpServletResponse.setStatus(HttpStatus.OK.value());
                return true;
            }
            return super.preHandle(request, response);
        }

        private static void setReturnInfo(HttpServletResponse response, int status, String msg) throws IOException {
            response.setContentType("application/json;charset=utf-8");
            Map<String, String> result = new HashMap<>();
            result.put("status", String.valueOf(status));
            result.put("msg", msg);
            response.getWriter().write(JSONUtils.toJSONString(result));
        }
复制代码

第四:配置shiro

    @Configuration
    public class ShiroConfig {

        @Bean("shiroFilter")
        public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager){
            ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
            shiroFilterFactoryBean.setSecurityManager(securityManager);
            Map<String, Filter> filterMap = new HashMap<>();
            filterMap.put("jwt",new JwtFilter());
            shiroFilterFactoryBean.setFilters(filterMap);
            Map<String,String> filterChainDefinitionMap = new LinkedHashMap<>();
            filterChainDefinitionMap.put("/user/login","anon");
            filterChainDefinitionMap.put("/**", "jwt");
            shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
            return shiroFilterFactoryBean;
        }
        // 禁用session
        @Bean
        protected SessionStorageEvaluator sessionStorageEvaluator(){
            DefaultWebSessionStorageEvaluator sessionStorageEvaluator = new DefaultWebSessionStorageEvaluator();
            sessionStorageEvaluator.setSessionStorageEnabled(false);
            return sessionStorageEvaluator;
        }

        @Bean("securityManager")
        public SecurityManager securityManager(MyRealm userRealm) {
            DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
            // 指定SecurityManager的域
            securityManager.setRealm(userRealm);
            /*
             * 关闭shiro自带的session,详情见文档
             * http://shiro.apache.org/session-management.html#SessionManagement-StatelessApplications%28Sessionless%29
             */
            DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
            DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator();
            defaultSessionStorageEvaluator.setSessionStorageEnabled(false);
            subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator);
            securityManager.setSubjectDAO(subjectDAO);

            return securityManager;
        }

        @Bean("myRealm")
        public MyRealm shiroRealm() {
            MyRealm shiroRealm = new MyRealm();
            return shiroRealm;
        }

        @Bean
        public DefaultAdvisorAutoProxyCreator getDefaultAdvisorAutoProxyCreator() {
            DefaultAdvisorAutoProxyCreator autoProxyCreator = new DefaultAdvisorAutoProxyCreator();
            autoProxyCreator.setProxyTargetClass(true);
            return autoProxyCreator;
        }
        /**
         * 开启shiro aop注解支持.
         * 使用代理方式;所以需要开启代码支持;
         * @param
         * @return
         */
        @Bean
        public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
            AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
            authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
            return authorizationAttributeSourceAdvisor;
        }

    }
复制代码

第五步在controller的方法上适用shiro权限控制的注解即可

    @GetMapping("list")
    // @RequiresAuthentication
    @RequiresRoles(value = {"admin"})
    // @RequiresPermissions(value = {"user:list"})
    public List<UserModel> listUsers(){
        return  userServiceInt.listUser();
    }
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

Springboot+Shiro+Jwt实现权限控制 的相关文章

随机推荐

  • 【网络编程】传输层协议——UDP协议

    文章目录 一 传输层的意义 二 端口号 2 1 五元组标识一个通信 2 2 端口号范围划分 2 3 知名端口号 2 4 绑定端口号数目问题 2 5 pidof netstat命令 三 UDP协议 3 1 UDP协议格式 3 2 如何理解报头
  • TortoiseGit如何迁移项目地址

    大家工作中可能会遇到项目迁服务器 那么在以前老服务器上的git项目也需要迁到新服务器 如果大家使用TortoiseGit 那么该如何迁移呢 很简单 一 首先在新服务器git上建个项目 然后把项目地址复制下来 二 在本地项目里找到 git文件
  • 你需要知道的企业网页制作流程

    企业网页制作是企业建立线上形象和宣传的重要手段之一 它不仅可以提高企业的品牌知名度 还可以扩大企业的影响力和拓展客户群 下面 我们将介绍一些企业网页制作的基本流程和技巧 并结合一个案例来详细解析 企业网页制作的基本流程可以分为以下几个步骤
  • Android自定义View(三) Scroller与平滑滚动

    目录 一 什么是Scroller 二 认识scrollTo和scrollBy方法 2 1 scrollTo scrollBy对View内容的影响 2 2 思考为什么移动负数距离会向坐标正方向移动 2 3 scrollTo scrollBy对
  • 预训练网络的模型微调方法

    是什么 神经网络需要数据来训练 从数据中获得信息 进而转化成相应的权重 这些权重能够被提取出来 迁移到其他的神经网络中 迁移学习 通过使用之前在大数据集上经过训练的预训练模型 我们可以直接使用相应的结构和权重 将他们应用在我们正在面对的问题
  • python字符串去重_Python对字符串实现去重操作的方法示例

    前言 最近在工作经常会碰到对字符串进行去重操作 下面就给大家列出用Python如何处理的 话不多说了 来一起看看详细的介绍吧 比如说 要拿下面的字符传去掉重复的AA A B C S AA BB EE DD AA A B C CC A B C
  • chatgpt赋能python:用Python根据IP地址查找具体地址:一个实用的SEO技巧

    用Python根据IP地址查找具体地址 一个实用的SEO技巧 在数字化时代 IP地址是一个重要的数字标识符 使用IP地址来发现目标受众的位置是网络营销和搜索引擎优化策略的重要组成部分 在本文中 我们将介绍用Python编程语言根据IP地址查
  • 前端页面无法渲染两个Google Translate插件

    在w3c官方给出的demo里尝试了一下 无论添加几个Google Translate标签 始终只会渲染首个Google Translate标签 需要在script标签里添加一下代码 即可解决
  • python人工智能项目开发实例-人工智能学习路线图,整理近200个实战案例与项目...

    Created by 唐宇迪 人工智能实战就业 面试 学习路线图 这个项目是干什么的 整理这个项目的初衷是方便同学们快速开启人工智能自学计划 在学习过程中少走弯路用最快的效率入门Ai并开始实战项目 提供了近200个Ai实战案例和项目 这些并
  • geth指定版本安装

    下载源码 cd usr local git clone https github com ethereum go ethereum git 或者 mirror git clone https ghproxy com https github
  • TRC-20合约

    pragma solidity 0 5 0 import IERC20 sol import SafeMath sol dev Implementation of the IERC20 interface This implementati
  • List从服务器取文件名乱码,急用!!!为什么list程序输出会乱码??

    该楼层疑似违规已被系统折叠 隐藏此楼查看此楼 window 15 2 78 23 textbackground LIGHTGREEN textcolor LIGHTMAGENTA clrscr gotoxy 1 3 printf Delet
  • Android学习笔记(一)——Activity基础知识

    Activity基础知识 Activity 活动 是一种可以包含用户界面的组件 一个应用程序可以包含多个活动 Android项目中逻辑和试图分离 每个活动最好对应一个布局 布局是用来显示界面内容的 Activity和布局文件如何关联起来 在
  • PyTorch基础-交叉熵函数mnist数据集识别-04

    交叉熵 代码 import numpy as np import torch from torch import nn optim from torch autograd import Variable from torchvision i
  • Spring框架详解(三)

    一 代理模式 代理模式 为一个对象提供一个替身 以控制对这个对象的访问 即通过代理对象访问目标对象 这样做的好处就是 可以在目标对象实现的基础上 增强额外的功能操作 即扩展目标对象的功能 被代理对象可以是远程对象 创建开销大的对象或者是需要
  • 批量上传图片

    前端使用jQuery完成图片的上传 后端使用Java接收图片文件 具体实现步骤 使用HTML和CSS进行页面的布局 并引入jQuery库 使用三个input标签 类型均为type file 的标签 并命名好相对应的id 这个id可以命名的有
  • 数睿通2.0功能发布—集成Doris、数仓动态配置、质量任务完善、接入速度优化

    文章目录 引言 集成 Doris 数仓动态配置 质量任务完善 接入速度优化 结语 引言 数睿通 2 0 数据中台迎来了 6 月份的更新 本次更新集成了 Doris 数据库 可用于数据集成 数据开发等模块 过去数仓使用的是 TiDB 新版本可
  • python获取浏览器 cookies

    python 获取浏览器 cookie代码 import browser cookie3 dict1 browser cookie3 chrome domain name baidu com print dict1 s for u in d
  • 【iOS-iap防护】验证用户付费收据!拒绝iap Cracker!!让iphone越狱用户无从下手!!!

    转载自 黑米GameDev街区 原文链接 http www himigame com iphone cocos2d 673 html 对于iOS的应用安全这块主要有两块是我们开发者可以避免的 一个是存储数据加密 这个在上一篇文章Himi介绍
  • Springboot+Shiro+Jwt实现权限控制

    前置背景 为什么写下这篇文章 因为需要实现一个设备管理系统的权限管理模块 在查阅很多博客以及其他网上资料之后 发现重复 无用的博客很多 因此写一篇文章来记录 以便后面复习 涉及的知识点主要有下列知识点 JWT shiro 书写顺序 首先使用