天天看點

Springboot+Shiro+Jwt實作簡單的權限控制

作者:晾幹的紅領巾
Springboot+Shiro+Jwt實作簡單的權限控制

前置背景

為什麼寫下這篇文章?

因為需要實作一個裝置管理系統的權限管理子產品,在查閱很多部落格以及其他網上資料之後,發現重複、無用的部落格很多,是以寫一篇文章來記錄,以便後面複習。

  1. 涉及的知識點主要有下列知識點:
    1. JWT
    2. shiro
  2. 書寫順序
    1. 首先使用springboot 結合 jwt完成前後端分離的token認證。
    2. 其次結合shiro完成shiro+jwt的前後端分離的權限認證管理。

權限管理的表結構設計

  1. 一個user可以擁有多個role,一個role也可以被多個user擁有, 一個 角色擁有多個權限即功能,一個權限可以被多個role擁有。
Springboot+Shiro+Jwt實作簡單的權限控制

使用者、角色、權限類

Springboot+Shiro+Jwt實作簡單的權限控制

表結構圖

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。 這三部分通過.連接配接,如下

Springboot+Shiro+Jwt實作簡單的權限控制

Token示例:

eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.
    eyJwYXNzd29yZCI6InN1cGVyIiwiZXhwIjoxNjYzMTIzNDgzLCJ1c2VybmFtZSI6InN1cGVyIn0.
    5xVg6IuOLe_uVwwOaeyRDbTHRjfmIbNsnb-DP9-Ic20
複制代碼           

如何使用:當使用者登入系統後,服務端給前端發送一個基于使用者資訊建立的token,然後在此後的每一次前端請求都會攜帶token。服務端通過攔截器攔截請求,同時驗證攜帶的token是否正确。如果正确則放行請求,不正确則拒絕通過。 思路流程圖:

Springboot+Shiro+Jwt實作簡單的權限控制

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的架構原理圖:

Springboot+Shiro+Jwt實作簡單的權限控制

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就能達到預期的效果。

思路流程圖:

Springboot+Shiro+Jwt實作簡單的權限控制

第一步改造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();
    }