Spring boot整合shiro+jwt 實現前後端分離

這裏內容很少很多都爲貼的代碼,具體內容我經過了看源碼和帖子加了註釋。帖子就沒用太多的內容
先下載shiro和jwt的jar包
<!-- shiro包 -->
<dependency>
    <groupId>org.apache.shiro</groupId>
    <artifactId>shiro-spring</artifactId>
    <version>1.4.0</version>
</dependency>
<dependency>
    <groupId>org.apache.shiro</groupId>
    <artifactId>shiro-ehcache</artifactId>
    <version>1.4.0</version>
</dependency>
<!--JWT依賴-->
<!--JWT-->
<dependency>
    <groupId>com.auth0</groupId>
    <artifactId>java-jwt</artifactId>
    <version>3.4.0</version>
</dependency>
<!--JJWT-->
<dependency>
     <groupId>io.jsonwebtoken</groupId>
     <artifactId>jjwt</artifactId>
     <version>0.9.0</version>
</dependency>

創建shiro的自定義的Realm

代碼如下:

package com.serverprovider.config.shiro.userRealm;


import com.spring.common.auto.autoUser.AutoUserModel;
import com.spring.common.auto.autoUser.extend.AutoModelExtend;
import com.serverprovider.config.shiro.jwt.JWTCredentialsMatcher;
import com.serverprovider.config.shiro.jwt.JwtToken;
import com.serverprovider.service.loginService.LoginServiceImpl;
import com.util.Redis.RedisUtil;
import com.util.ReturnUtil.SecretKey;
import com.util.encryption.JWTDecodeUtil;
import io.jsonwebtoken.Claims;
import org.apache.log4j.Logger;
import org.apache.shiro.authc.*;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.ExceptionHandler;

import java.util.HashSet;
import java.util.List;
import java.util.Set;

public class UserRealm extends AuthorizingRealm {

    private Logger logger = Logger.getLogger(UserRealm.class);


    @Autowired private LoginServiceImpl loginService;


    public UserRealm(){
        //這裏使用我們自定義的Matcher驗證接口
        this.setCredentialsMatcher(new JWTCredentialsMatcher());
    }

    /**
     * 必須重寫此方法,不然Shiro會報錯
     */
    @Override
    public boolean supports(AuthenticationToken token) {
        return token instanceof JwtToken;
    }

    /**
     *  shiro 身份驗證
     * @param token
     * @return  boolean
     * @throws AuthenticationException 拋出的異常將有統一的異常處理返回給前端
     *
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token)throws AuthenticationException {
        /**
         *  AuthenticationToken
         *  JwtToken重寫了AuthenticationToken接口 並創建了一個接口token的變量
         *   因爲在filter我們將token存入了JwtToken的token變量中
         *   所以這裏直接getToken()就可以獲取前端傳遞的token值
         */
            String JWTtoken = ((JwtToken) token).getToken();
        /**
         *  Claims對象它最終是一個JSON格式的對象,任何值都可以添加到其中
         *  token解密  轉換成Claims對象
         */
             Claims claims = JWTDecodeUtil.parseJWT(JWTtoken, SecretKey.JWTKey);
           
        /**
         *   根據JwtUtil加密方法加入的參數獲取數據
         *   查詢數據庫獲得對象
         *   如爲空:拋出異常
         *   如驗證失敗拋出 AuthorizationException
         */
            String username = claims.getSubject();
            String password =  (String) claims.get("password");
            AutoModelExtend principal = loginService.selectLoginModel(username,password);
            return new SimpleAuthenticationInfo(principal, JWTtoken,"userRealm");
    }


    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        SimpleAuthorizationInfo info = null;
        /**
         *  PrincipalCollection對象
         *  文檔裏面描述:返回從指定的Realm 僅作爲Collection 返回的單個Subject的對象,如果沒有來自該領域的任何對象,則返回空的Collection。
         *  在登錄接口放入權限註解返回的錯誤信息:Subject.login(AuthenticationToken)或SecurityManager啓用'Remember Me'功能後成功自動獲取這些標識主體
         *  當調用Subject.login()方法成功後 PrincipalCollection會自動獲得該對象  如沒有認證過或認證失敗則返回空的Collection並拋出異常
         * 	getPrimaryPrincipal():返回在應用程序範圍內使用的主要對象,以唯一標識擁有帳戶。
         */
        Object principal = principals.getPrimaryPrincipal();
        /**
         * 得到身份對象
         * 查詢該用戶的權限信息
         */
        AutoUserModel user = (AutoUserModel) principal;
        List<String> roleModels = loginService.selectRoleDetails(user.getId());
        try {
        /**
         * 創建一個Set,來放置用戶擁有的權限
         * 創建 SimpleAuthorizationInfo, 並將辦好權限列表的Set放入.
         */
        Set<String> rolesSet = new HashSet();
        for (String role : roleModels) {
            rolesSet.add(role);
        }
        info = new SimpleAuthorizationInfo();
        info.setStringPermissions(rolesSet);   // 放入權限信息
    }catch (Exception e){
        throw new AuthenticationException("授權失敗!");
    }
        return info;
    }
}

這個授權方法遇到的坑比較少,就是在最終驗證的時候網上很照抄過來的帖子一點都沒有驗證就粘貼賦值,在這裏嚴重吐槽。

在使用jwt最爲token而取消shiro傳統的session時候,我們的需要重寫shiro的驗證接口   CredentialsMatcher,在 自定義的realm

中我們加入我們重寫的驗證方法,在調用SimpleAuthenticationInfo()方法進行驗證的時候,shiro就會使用重寫的驗證接口。

此處爲大坑。

貼上代碼如下:



import com.spring.common.auto.autoUser.extend.AutoModelExtend;
import org.apache.log4j.Logger;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.credential.CredentialsMatcher;



/**
 *  CredentialsMatcher
 *  接口由類實現,該類可以確定是否提供了AuthenticationToken憑證與系統中存儲的相應帳戶的憑證相匹配。
 *  Shiro 加密匹配 重寫匹配方法CredentialsMatcher  使用JWTUtil 匹配方式
 */
public class JWTCredentialsMatcher  implements CredentialsMatcher {

    private Logger logger = Logger.getLogger(JWTCredentialsMatcher.class);

    /**
     * Matcher中直接調用工具包中的verify方法即可
     */
    @Override
    public boolean doCredentialsMatch(AuthenticationToken authenticationToken, AuthenticationInfo authenticationInfo) {
        String token = (String) ((JwtToken)authenticationToken).getToken();
        AutoModelExtend user = (AutoModelExtend)authenticationInfo.getPrincipals().getPrimaryPrincipal();
        //得到DefaultJwtParser
        Boolean verify = JwtUtil.isVerify(token, user);
        logger.info("JWT密碼效驗結果="+verify);
        return verify;
    }
}

shiro的配置項  ShiroConfiguration代碼如下:


import com.serverprovider.config.shiro.shiroSysFile.JwtFilter;
import com.serverprovider.config.shiro.userRealm.UserRealm;
import org.apache.log4j.Logger;
import org.apache.shiro.mgt.DefaultSessionStorageEvaluator;
import org.apache.shiro.mgt.DefaultSubjectDAO;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.mgt.SessionStorageEvaluator;
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.apache.shiro.web.mgt.DefaultWebSessionStorageEvaluator;

import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import javax.servlet.Filter;
import java.util.*;

@Configuration
public class ShiroConfiguration {

    private Logger logger = Logger.getLogger(ShiroConfiguration.class);



    @Bean("shiroFilter")
    public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager) {
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        shiroFilterFactoryBean.setSecurityManager(securityManager);
        //攔截器
        Map<String, String> filterChainDefinitionMap = new LinkedHashMap<String, String>();
        // 配置不會被攔截的鏈接 順序判斷
        filterChainDefinitionMap.put("/login/**", "anon");
        // 添加自己的過濾器並且取名爲jwt
        Map<String, Filter> filterMap = new HashMap<String, Filter>();
        filterMap.put("jwt", new JwtFilter());
        shiroFilterFactoryBean.setFilters(filterMap);
        //<!-- 過濾鏈定義,從上向下順序執行,一般將/**放在最爲下邊
        filterChainDefinitionMap.put("/**", "jwt");
        shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);

        return shiroFilterFactoryBean;
    }

    /**
     * 禁用session, 不保存用戶登錄狀態。保證每次請求都重新認證。
     * 需要注意的是,如果用戶代碼裏調用Subject.getSession()還是可以用session
     */
    @Bean
    protected SessionStorageEvaluator sessionStorageEvaluator(){
        DefaultWebSessionStorageEvaluator sessionStorageEvaluator = new DefaultWebSessionStorageEvaluator();
        sessionStorageEvaluator.setSessionStorageEnabled(false);
        return sessionStorageEvaluator;
    }


    @Bean("securityManager")
    public SecurityManager securityManager(UserRealm userRealm) {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        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;
    }
    /**
     * 創建自定義的UserRealm @bean
     */
    @Bean("userRealm")
    public UserRealm shiroRealm() {
        UserRealm shiroRealm = new UserRealm();
        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;
    }

到這裏shiro配置就全部完成了

下面開始配置jwt:

首先我們需要重寫  AuthenticationToken接口 此接口的作用

負責把shiro中username,password生成用於驗證的token的封裝類

所有我們需要去實現這個接口,封裝我們自己生成的JWT生成的token

import org.apache.shiro.authc.AuthenticationToken;

/**
 *  AuthenticationToken: shiro中負責把username,password生成用於驗證的token的封裝類
 *  我們需要自定義一個對象用來包裝token。
 */
public class JwtToken implements AuthenticationToken {

    private String token;

    public JwtToken(String token) {
        this.token = token;
    }

    public String getToken() {
        return token;
    }

    public void setToken(String token) {
        this.token = token;
    }

    @Override
    public Object getPrincipal() {
        return null;
    }

    @Override
    public Object getCredentials() {
        return null;
    }
}

因爲我們是前後端分離項目所有我們不存在session驗證之類的 所有每次請求都需要攜帶token,所以每次請求我們都需要去驗證token的真實性,所有我們需要去實現  BasicHttpAuthenticationFilter過濾器  

BasicHttpAuthenticationFilter繼承 AuthenticatingFilter 過濾器其能夠自動地進行基於所述傳入請求的認證嘗試。此實現是每個基本HTTP身份驗證規範的Java實現  , 通過此過濾器得到HTTP請求資源獲取Authorization傳遞過來的token參數   獲取subject對象進行身份驗證

代碼如下:


import com.alibaba.fastjson.JSONObject;
import com.serverprovider.config.shiro.jwt.JwtToken;
import com.util.Util.utilTime;
import org.apache.log4j.Logger;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RequestMethod;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.concurrent.TimeUnit;

/**
 *
 * BasicHttpAuthenticationFilter繼承 AuthenticatingFilter 過濾器
 * 其能夠自動地進行基於所述傳入請求的認證嘗試。
 *       BasicHttpAuthenticationFilter 基本訪問認證過濾器
 *      此實現是每個基本HTTP身份驗證規範的Java實現
 *      通過此過濾器得到HTTP請求資源獲取Authorization傳遞過來的token參數
 *      獲取subject對象進行身份驗證
 *
 *
 */
public class JwtFilter extends BasicHttpAuthenticationFilter {

    Logger logger = Logger.getLogger(JwtFilter.class);

    /**
     *  應用的HTTP方法列表配置基本身份驗證篩選器。
     *  獲取 request 請求 拒絕攔截登錄請求
     *  執行登錄認證方法
     *
     * @param request
     * @param response
     * @param mappedValue
     * @return
     */
    @Override
    protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
        HttpServletRequest httpServletRequest = (HttpServletRequest) request;
        String requestURI = httpServletRequest.getRequestURI();
        if (requestURI.equals("/user/login/verifyUser") || requestURI.equals("/user/register")) {
            return true;
        } else {
            try {
                executeLogin(request, response);
                return true;
            } catch (Exception e) {
                e.printStackTrace();
                return false;
            }
        }
    }



    /**
     *  Authorization攜帶的參數爲token
     *  JwtToken實現了AuthenticationToken接口封裝了token參數
     *  通過getSubject方法獲取 subject對象
     *  login()發送身份驗證
     *
     *  爲什麼需要在Filter中調用login,不能在controller中調用login?
     *  由於Shiro默認的驗證方式是基於session的,在基於token驗證的方式中,不能依賴session做爲登錄的判斷依據.
     * @param request
     * @param  response
     */
    @Override
    protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception {
        HttpServletResponse httpServletResponse = (HttpServletResponse) response;
        try{
            HttpServletRequest httpServletRequest = (HttpServletRequest) request;
            String token = httpServletRequest.getHeader("Authorization");
            JwtToken jwtToken = new JwtToken(token);
            // 提交給realm進行登入,如果錯誤他會拋出異常並被捕獲
            Subject subject = getSubject(request, response);
            subject.login(jwtToken);
            logger.info("JWT驗證用戶信息成功");
            // 如果沒有拋出異常則代表登入成功,返回true
            return true;
            }catch (Exception e){
           /* *
             *  這個問題糾結了好久
             *      原生的shiro驗證失敗會進入全局異常 但是 和JWT結合以後卻不進入了  之前一直想不通
             *      原因是 JWT直接在過濾器裏驗證  驗證成功與否 都是直接返回到過濾器中 成功在進入controller
             *      失敗直接返回進入springboot自定義異常處理頁面
             */
            JSONObject responseJSONObject = new JSONObject();
            responseJSONObject.put("result","401");
            responseJSONObject.put("resultCode","token無效,請重新獲取。");
            responseJSONObject.put("resultData","null");
            responseJSONObject.put("resultTime", utilTime.StringDate());
            PrintWriter out = null;
            httpServletResponse.setCharacterEncoding("UTF-8");
            httpServletResponse.setContentType("application/json; charset=utf-8");
            logger.info("返回是");
            logger.info(responseJSONObject.toString());
            out = httpServletResponse.getWriter();
            out.append(responseJSONObject.toString());
        }
    return false;
    }



    /**
     * 對跨域提供支持
     */
    @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"));
        // 跨域時會首先發送一個option請求,這裏我們給option請求直接返回正常狀態
        if (httpServletRequest.getMethod().equals(RequestMethod.OPTIONS.name())) {
            httpServletResponse.setStatus(HttpStatus.OK.value());
            return false;
        }
        return super.preHandle(request, response);
    }

}

貼上 加密解密校驗的工具類

import com.spring.common.auto.autoUser.extend.AutoModelExtend;
import com.util.ReturnUtil.SecretKey;
import io.jsonwebtoken.*;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;;

/* @author hw
 * @create 2019-04-16 10.12
 * @desc JWT工具類
 **/

public class JwtUtil {


    /**
     * 用戶登錄成功後生成Jwt
     * 使用Hs256算法  私匙使用用戶密碼
     *
     * @param ttlMillis jwt過期時間
     * @param user      登錄成功的user對象
     * @return
     */
    public static String createJWT(long ttlMillis, AutoModelExtend user) {
        //指定簽名的時候使用的簽名算法,也就是header那部分,jjwt已經將這部分內容封裝好了。
        SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
        //生成JWT的時間
        long nowMillis = System.currentTimeMillis();
        Date now = new Date(nowMillis);

        //創建payload的私有聲明(根據特定的業務需要添加,如果要拿這個做驗證,一般是需要和jwt的接收方提前溝通好驗證方式的)
        Map<String, Object> map = new HashMap<String, Object>();
        map.put("id", user.getId());
        map.put("username", user.getAuto_username());
        map.put("password", user.getAuto_password());

        //生成簽名的時候使用的祕鑰secret,這個方法本地封裝了的,一般可以從本地配置文件中讀取,
        // 切記這個祕鑰不能外露哦。它就是你服務端的私鑰,在任何場景都不應該流露出去。一旦客戶端得知這個secret, 那就意味着客戶端是可以自我簽發jwt了。
        String key = SecretKey.JWTKey;
        //生成簽發人
        String subject = user.getAuto_username();


        //下面就是在爲payload添加各種標準聲明和私有聲明瞭
        //這裏其實就是new一個JwtBuilder,設置jwt的body
        JwtBuilder builder = Jwts.builder()
                //如果有私有聲明,一定要先設置這個自己創建的私有的聲明,這個是給builder的claim賦值,一旦寫在標準的聲明賦值之後,就是覆蓋了那些標準的聲明的
                .setClaims(map)
                //設置jti(JWT ID):是JWT的唯一標識,根據業務需要,這個可以設置爲一個不重複的值,主要用來作爲一次性token,從而回避重放攻擊。
                .setId(UUID.randomUUID().toString())
                //iat: jwt的簽發時間
                .setIssuedAt(now)
                //代表這個JWT的主體,即它的所有人,這個是一個json格式的字符串,可以存放什麼userid,roldid之類的,作爲什麼用戶的唯一標誌。
                .setSubject(subject)
                //設置簽名使用的簽名算法和簽名使用的祕鑰
                .signWith(signatureAlgorithm, key);
        if (ttlMillis >= 0) {
            long expMillis = nowMillis + ttlMillis;
            Date exp = new Date(expMillis);
            //設置過期時間
            builder.setExpiration(exp);
        }
        return builder.compact();
    }


    /**
     * 校驗token
     * 在這裏可以使用官方的校驗,我這裏校驗的是token中攜帶的密碼於數據庫一致的話就校驗通過
     *
     * @param token
     * @return
     */
    public static Boolean isVerify(String token, AutoModelExtend userModelExtend) {
        try {
            //得到DefaultJwtParser
            Claims claims = Jwts.parser()
                    //設置簽名的祕鑰
                    .setSigningKey(SecretKey.JWTKey)
                    //設置需要解析的jwt
                    .parseClaimsJws(token).getBody();

            if (claims.get("password").equals(userModelExtend.getAuto_password())) {
                return true;
            }
        } catch (Exception exception) {
            return false;
        }
        return null;

    }
 /**
     * Token的解密
     * @param token 加密後的token
     * @param secret  簽名祕鑰,和生成的簽名的祕鑰一模一樣
     * @return
     */
    public static Claims parseJWT(String token, String  secret) {
        //得到DefaultJwtParser
        Claims claims = Jwts.parser()
                //設置簽名的祕鑰
                .setSigningKey(secret)
                //設置需要解析的jwt
                .parseClaimsJws(token).getBody();
        return claims;
    }
}

到這裏shiro jwt整合就完成了  

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章