Spring-boot、Shrio實現JWT

關於驗證大體分爲兩個方面:

  1. 用戶登陸時的驗證;
  2. 用戶登陸後每次訪問時的權限認證

主要解決方法:使用自定義的Shiro Filter

項目搭建:

這是一個spring-boot 的web項目,不瞭解spring-boot的項目搭建,請google。
  • pom.mx引入相關jar


<!-- shiro 權限管理 -->
    <dependency>
        <groupId>org.apache.shiro</groupId>
        <artifactId>shiro-spring</artifactId>
        <version>${shiro.version}</version>
    </dependency>
    <dependency>
        <groupId>org.apache.shiro</groupId>
        <artifactId>shiro-core</artifactId>
        <version>${shiro.version}</version>
    </dependency>
 <!-- JWT -->
     <dependency>
        <groupId>io.jsonwebtoken</groupId>
        <artifactId>jjwt</artifactId>
        <version>0.9.0</version>
    </dependency>
  • Shrio 的相關配置

劃重點!!自定義了一個Filterhtml

filterMap.put("JWTFilter", new JWTFilter());java

@Configuration
public class ShiroConfig {

    @Bean
    public ShiroFilterFactoryBean getShiroFilterFactoryBean(SecurityManager securityManager) {
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        shiroFilterFactoryBean.setSecurityManager(securityManager);
        // 添加本身的過濾器而且取名爲JWTFilter
        Map<String, Filter> filterMap = new HashMap<>();
        filterMap.put("JWTFilter", new JWTFilter());
        shiroFilterFactoryBean.setFilters(filterMap);
        /*
         * 自定義url規則
         * http://shiro.apache.org/web.html#urls-
         */
        Map<String, String> filterChainDefinitionMap = shiroFilterFactoryBean.getFilterChainDefinitionMap();
        filterChainDefinitionMap.put("/**", "JWTFilter");
        shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
        return shiroFilterFactoryBean;
    }


    /**
     * securityManager 不用直接注入shiroDBRealm,可能會致使事務失效
     * 解決方法見 handleContextRefresh
     * http://www.debugrun.com/a/NKS9EJQ.html
     */
    @Bean("securityManager")
    public DefaultWebSecurityManager securityManager(TokenRealm tokenRealm) {
        DefaultWebSecurityManager manager = new DefaultWebSecurityManager();
        manager.setRealm(tokenRealm);
        /*
         * 關閉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);
        manager.setSubjectDAO(subjectDAO);
        return manager;
    }

    @Bean
    public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
        return new LifecycleBeanPostProcessor();
    }

    @Bean(name = "TokenRealm")
    @DependsOn("lifecycleBeanPostProcessor")
    public TokenRealm tokenRealm() {
        return new TokenRealm();
    }

    @Bean
    @DependsOn("lifecycleBeanPostProcessor")
    public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
        DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
        // 強制使用cglib,防止重複代理和可能引發代理出錯的問題
        // https://zhuanlan.zhihu.com/p/29161098
        defaultAdvisorAutoProxyCreator.setProxyTargetClass(true);
        return defaultAdvisorAutoProxyCreator;
    }

    @Bean
    public AuthorizationAttributeSourceAdvisor getAuthorizationAttributeSourceAdvisor(SecurityManager securityManager) {
        AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
        authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
        return new AuthorizationAttributeSourceAdvisor();
    }
}
  • 自定義Shrio filter

執行順序:preHandle -> doFilterInternal -> executeLogin -> onLoginSuccess

主要判斷是否是登陸請求的是 doFilterInternal
public class JWTFilter extends BasicHttpAuthenticationFilter {

    /**
     * 自定義執行登陸的方法
     */
    @Override
    protected boolean executeLogin(ServletRequest request, ServletResponse response) throws IOException {
        HttpServletRequest httpServletRequest = (HttpServletRequest) request;
        UsernamePasswordToken usernamePasswordToken = JSON.parseObject(httpServletRequest.getInputStream(), UsernamePasswordToken.class);
        // 提交給realm進行登入,若是錯誤他會拋出異常並被捕獲
        Subject subject = this.getSubject(request, response);
        subject.login(usernamePasswordToken);
        return this.onLoginSuccess(usernamePasswordToken, subject, request, response);
        //錯誤拋出異常
    }

    /**
     * 最早執行的方法
     */
    @Override
    protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {
        return super.preHandle(request, response);
    }

    /**
     * 登陸成功後登陸的操做
     * 加上jwt 的header
     */
    @Override
    protected boolean onLoginSuccess(AuthenticationToken token, Subject subject, ServletRequest request, ServletResponse response) {
        HttpServletResponse httpServletResponse = (HttpServletResponse) response;
        String jwtToken = Jwts.builder()
                .setId(token.getPrincipal().toString())
                .setExpiration(DateTime.now().plusMinutes(30).toDate())
                .signWith(SignatureAlgorithm.HS256, JWTCost.signatureKey)
                .compact();
        httpServletResponse.addHeader(AUTHORIZATION_HEADER, jwtToken);
        return true;
    }

    /**
     * 登陸以及校驗的主要流程
     * 判斷是不是登陸,或者是登錄後普通的一次請求
     */
    @Override
    public void doFilterInternal(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequest;
        HttpServletResponse httpServletResponse = (HttpServletResponse) servletResponse;

        String servletPath = httpServletRequest.getServletPath();
        if (StringUtils.equals(servletPath, "/login")) {
            //執行登陸
            this.executeLogin(servletRequest, servletResponse);
        } else {
            String authenticationHeader = httpServletRequest.getHeader(AUTHORIZATION_HEADER);
            if (StringUtils.isNotEmpty(authenticationHeader)) {

                Claims body = Jwts.parser()
                        .setSigningKey(JWTCost.signatureKey)
                        .parseClaimsJws(authenticationHeader)
                        .getBody();
                if (body != null) {
                    //更新token
                    body.setExpiration(DateTime.now().plusMinutes(30).toDate());
                    String updateToken = Jwts.builder().setClaims(body).compact();
                    httpServletResponse.addHeader(AUTHORIZATION_HEADER, updateToken);

                    //添加用戶憑證
                    PrincipalCollection principals = new SimplePrincipalCollection(body.getId(), JWTCost.UserNamePasswordRealm);//拼裝shiro用戶信息
                    WebSubject.Builder builder = new WebSubject.Builder(servletRequest, servletResponse);
                    builder.principals(principals);
                    builder.authenticated(true);
                    builder.sessionCreationEnabled(false);
                    WebSubject subject = builder.buildWebSubject();
                    //塞入容器,統一調用
                    ThreadContext.bind(subject);
                    filterChain.doFilter(httpServletRequest, httpServletResponse);
                }
            } else {
                httpServletResponse.setStatus(HttpStatus.FORBIDDEN.value());
            }
        }
    }
}
  • 登陸失敗處理

處理Shrio異常

@RestControllerAdvice
public class GlobalControllerExceptionHandler {

    @ExceptionHandler(value = Exception.class)
    public Object allExceptionHandler(HttpServletRequest request, HttpServletResponse response, Exception exception) {
        String message = exception.getCause().getMessage();
        LogUtil.error(message);
        return new ResultInfo(exception.getClass().getName(), message);
    }

    /*=========== Shiro 異常攔截==============*/

    @ExceptionHandler(value = IncorrectCredentialsException.class)
    public String IncorrectCredentialsException(HttpServletRequest request, HttpServletResponse response, Exception exception) {
        response.setStatus(HttpStatus.FORBIDDEN.value());
        return "IncorrectCredentialsException";
    }

    @ExceptionHandler(value = UnknownAccountException.class)
    public String UnknownAccountException(HttpServletRequest request, HttpServletResponse response, Exception exception) {
        response.setStatus(HttpStatus.FORBIDDEN.value());
        return "UnknownAccountException";
    }

    @ExceptionHandler(value = LockedAccountException.class)
    public String LockedAccountException(HttpServletRequest request, HttpServletResponse response, Exception exception) {
        response.setStatus(HttpStatus.FORBIDDEN.value());
        return "LockedAccountException";
    }

    @ExceptionHandler(value = ExcessiveAttemptsException.class)
    public String ExcessiveAttemptsException(HttpServletRequest request, HttpServletResponse response, Exception exception) {
        response.setStatus(HttpStatus.FORBIDDEN.value());
        return "ExcessiveAttemptsException";
    }

    @ExceptionHandler(value = AuthenticationException.class)
    public String AuthenticationException(HttpServletRequest request, HttpServletResponse response, Exception exception) {
        response.setStatus(HttpStatus.FORBIDDEN.value());
        return "AuthenticationException";
    }

    @ExceptionHandler(value = UnauthorizedException.class)
    public String UnauthorizedException(HttpServletRequest request, HttpServletResponse response, Exception exception) {
        response.setStatus(HttpStatus.FORBIDDEN.value());
        return "UnauthorizedException";
    }
}

處理JWT異常

這是個坑,由於是在filter內發生的異常,@ExceptionHandler是截獲不到的。

/**
 * 截獲spring boot Error頁面
 */
@RestController
public class GlobalExceptionHandler implements ErrorController {
    @Override
    public String getErrorPath() {
        return "/error";
    }

    @RequestMapping(value = "/error")
    public Object error(HttpServletRequest request, HttpServletResponse response) throws Exception {
        // 錯誤處理邏輯
        Exception exception = (Exception) request.getAttribute("javax.servlet.error.exception");
        Throwable cause = exception.getCause();
        if (cause instanceof ExpiredJwtException) {
            response.setStatus(HttpStatus.GATEWAY_TIMEOUT.value());
            return new ResultInfo("ExpiredJwtException", cause.getMessage());
        }
        if (cause instanceof MalformedJwtException) {
            response.setStatus(HttpStatus.FORBIDDEN.value());
            return new ResultInfo("MalformedJwtException", cause.getMessage());
        }
        return new ResultInfo(cause.getCause().getMessage(), cause.getMessage());
    }
}

關於權限等受權信息,能夠直接放到Redis中實現緩存。我認爲也是不錯的。

源碼奉上:githup-shiro分支 :舒適提示:平時測試代碼可能比較亂。git

若是更好的實現,讓我學習,讓我我進步,請聯繫我。
相關文章
相關標籤/搜索