Springboot+Shiro+Jwt实现简单的权限控制

  • Post author:
  • Post category:其他


前置背景

  • 为什么写下这篇文章?

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

  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();
    }



版权声明:本文为weixin_39158966原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。