java

关注公众号 jb51net

关闭
首页 > 软件编程 > java > Springboot Shiro Jwt权限控制

Springboot+Shiro+Jwt实现权限控制的项目实践

作者:搬山道猿

如今的互联网已经成为前后端分离的时代,所以本文在使用SpringBoot整合Shiro框架的时候会联合JWT一起搭配使用,具有一定的参考价值,感兴趣的可以了解一下

前置背景

为什么写下这篇文章?

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

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

书写顺序

权限管理的表结构设计

一个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>

思路整理

在前后端分离的项目中,由服务器使用的会话管理 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;
        }
    }

拦截器的创建和配置

创建拦截器,拦截请求

@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

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

就相当于在一个拥有多个公司的工业园区,人们需要有这个园区的卡片,才允许进入园区,而进入园区之后需要由本公司的门禁你才能进入公司,否则就不能进入公司一样。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();
    }

到此这篇关于Springboot+Shiro+Jwt实现权限控制的项目实践的文章就介绍到这了,更多相关Springboot Shiro Jwt实现权限控制内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

您可能感兴趣的文章:
阅读全文