先後分離,使用自定義token做爲shiro認證標識,實現springboot整合shiro

直接進入主題,項目是使用springboot,框架用的shiro作權限,mybatis作orm框架,項目須要作先後分離,這樣就會致使一個問題,shiro是根據sessionID來識別是否是同一個request,但若是先後分離的話,就會出現跨域的問題,session極可能就會發生變化,這樣就須要用一個標記來代表是同一個請求。初步的方案就是用token來代替session,但本質上說,如今的這種方式,仍是用的session的那一套,不過是對中間進行了處理,下面上代碼:前端

咱們要先解決的是跨域的問題:java

springboot解決跨域很好解決,以下便可。nginx

package com.common.config.cors;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;

/**
 * @author :LX
 * 建立時間: 2019/5/30. 14:03
 * 地點:廣州
 * 目的: 跨域訪問控制
 *          作先後分離的話,這個也是必配的
 * 備註說明:
 */
@Configuration
public class CorsConfig {

    private CorsConfiguration buildConfig() {
        CorsConfiguration corsConfiguration = new CorsConfiguration();
        // 容許任何域名使用
        corsConfiguration.addAllowedOrigin("*");
        // 容許任何頭
        corsConfiguration.addAllowedHeader("*");
        // 容許任何方法(post、get等)
        corsConfiguration.addAllowedMethod("*");
        return corsConfiguration;
    }


    @Bean
    public CorsFilter corsFilter() {
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        // 對接口配置跨域設置
        source.registerCorsConfiguration("/**", buildConfig());
        return new CorsFilter(source);
    }
}

而後自定義 realm ,簡單點說,就是你實現查詢用戶角色和權限的類。這一步就省略了,不外乎查詢數據庫,查詢當前用戶的角色和權限。web

接着自定義token,簡單的說,這裏其實就是讓前端請求的時候在請求頭中帶一個特定的標識,而後根據這個標識找到vlues,匹配上咱們的sessionId。redis

package com.common.config.shiro;

import org.apache.shiro.web.servlet.ShiroHttpServletRequest;
import org.apache.shiro.web.session.mgt.DefaultWebSessionManager;
import org.apache.shiro.web.util.WebUtils;
import org.springframework.util.StringUtils;

import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import java.io.Serializable;

/**
 * @author :LX
 * 建立時間: 2019/5/30. 18:08
 * 地點:廣州
 * 目的: shiro 的 session 管理
 *      自定義session規則,實現先後分離,在跨域等狀況下使用token 方式進行登陸驗證才須要,不然沒必須使用本類。
 *      shiro默認使用 ServletContainerSessionManager 來作 session 管理,它是依賴於瀏覽器的 cookie 來維護 session 的,調用 storeSessionId  方法保存sesionId 到 cookie中
 *      爲了支持無狀態會話,咱們就須要繼承 DefaultWebSessionManager
 *      自定義生成sessionId 則要實現 SessionIdGenerator
 * 備註說明:
 */
public class ShiroSession extends DefaultWebSessionManager {

    /**
     * 定義的請求頭中使用的標記key,用來傳遞 token
     */
    private static final String AUTH_TOKEN = "authToken";

    private static final String REFERENCED_SESSION_ID_SOURCE = "Stateless request";


    public ShiroSession() {
        super();
        //設置 shiro session 失效時間,默認爲30分鐘,這裏如今設置爲15分鐘
        //setGlobalSessionTimeout(MILLIS_PER_MINUTE * 15);
    }



    /**
     * 獲取sessionId,本來是根據sessionKey來獲取一個sessionId
     * 重寫的部分多了一個把獲取到的token設置到request的部分。這是由於app調用登錄接口的時候,是沒有token的,登錄成功後,產生了token,咱們把它放到request中,返回結
     * 果給客戶端的時候,把它從request中取出來,而且傳遞給客戶端,客戶端每次帶着這個token過來,就至關因而瀏覽器的cookie的做用,也就能維護會話了
     * @param request
     * @param response
     * @return
     */
    @Override
    protected Serializable getSessionId(ServletRequest request, ServletResponse response) {
        //獲取請求頭中的 AUTH_TOKEN 的值,若是請求頭中有 AUTH_TOKEN 則其值爲sessionId。shiro就是經過sessionId 來控制的
        String sessionId = WebUtils.toHttp(request).getHeader(AUTH_TOKEN);
        if (StringUtils.isEmpty(sessionId)){
            //若是沒有攜帶id參數則按照父類的方式在cookie進行獲取sessionId
            return super.getSessionId(request, response);

        } else {
            //請求頭中若是有 authToken, 則其值爲sessionId
            request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_SOURCE, REFERENCED_SESSION_ID_SOURCE);
            //sessionId
            request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID, sessionId);
            request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_IS_VALID, Boolean.TRUE);
            return sessionId;
        }
    }

}

你們看上面的代碼,其實就是咱們的請求頭中獲取  authToken 的值,而後塞入到sessionId中,代替了session.spring

若是你們還須要自定義這個token,或者說自定義生成的seesionId,就須要看下面的這個方法。數據庫

根據個人研究,最終找到 JavaUuidSessionIdGenerator 這個類,而後能夠找到apache

public Serializable generateId(Session session) {
		return UUID.randomUUID().toString();
	}

上面的代碼,其實就是生成了一串UUID,咱們能夠實現SessionIdGenerator接口來完成自定義的sessionID生成json

public class UuidSessionIdGenerator implements SessionIdGenerator{
 
	@Override
	public Serializable generateId(Session session) {
		Serializable uuid = new JavaUuidSessionIdGenerator().generateId(session);
		GGLogger.info("生成的sessionid是:"+uuid);
		return uuid;
	}
 
}
###自定義生成sessionid
sessionIdGenerator=ggauth.shiro.user.common.UuidSessionIdGenerator
securityManager.sessionManager.sessionDAO.sessionIdGenerator=$sessionIdGenerator

上面自定義生成的代碼是 參考的 https://blog.csdn.net/yaomingyang/article/details/78142763 的代碼,未通過驗證,但應該是沒問題的。我這裏是沒有去修改生成UUID的邏輯。後端

其實配置了這2個東西以後,就能夠弄shiro最終的配置了。

package com.common.config.shiro;

import com.yunji.kwxt.common.Constant;
import com.yunji.kwxt.common.filter.CORSAuthenticationFilter;
import org.apache.shiro.cache.ehcache.EhCacheManager;
import org.apache.shiro.session.mgt.SessionManager;
import org.apache.shiro.session.mgt.eis.EnterpriseCacheSessionDAO;
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import javax.servlet.Filter;
import java.util.LinkedHashMap;
import java.util.Map;

/**
 * @author :LX
 * 建立時間: 2019/5/27. 11:39
 * 地點:廣州
 * 目的: shiro配置
 * 備註說明:
 */
@Configuration
public class ShiroConfig {

    private static Logger log = LoggerFactory.getLogger(ShiroConfig.class);




    /**
     * 對shiro的攔截器進行注入
     * <p>
     * securityManager:
     * 全部Subject 實例都必須綁定到一個SecurityManager上,SecurityManager 是 Shiro的核心,初始化時協調各個模塊運行。然而,一旦 SecurityManager協調完畢,
     * SecurityManager 會被單獨留下,且咱們只須要去操做Subject便可,無需操做SecurityManager 。 可是咱們得知道,當咱們正與一個 Subject 進行交互時,實質上是
     * SecurityManager在處理 Subject 安全操做
     *
     * @param securityManager
     * @return
     */
    @Bean
    public ShiroFilterFactoryBean shiroFilter(org.apache.shiro.mgt.SecurityManager securityManager) {
        ShiroFilterFactoryBean shiroFilter = new ShiroFilterFactoryBean();
        shiroFilter.setSecurityManager(securityManager);

        //設置遇到未登陸、未受權等狀況時候,請求這些地址,返回相應的錯誤
        shiroFilter.setLoginUrl("/user/shiroError?errorId=" + Constant.NEED_LOGIN);
        shiroFilter.setUnauthorizedUrl("/user/shiroError?errorId=" + Constant.NO_UNAUTHORIZED);

        //攔截器,配置訪問權限 必須是LinkedHashMap,由於它必須保證有序。濾鏈定義,從上向下順序執行,通常將 /**放在最爲下邊
        Map<String, String> filterMap = new LinkedHashMap<String, String>();

        // 配置不會被攔截的連接 順序判斷
        filterMap.put("/user/login", "anon");
        filterMap.put("/user/shiroError", "anon");
        filterMap.put("/user/reg", "anon");

        //剩餘的請求shiro都攔截
        filterMap.put("/**/*", "authc");

        shiroFilter.setFilterChainDefinitionMap(filterMap);


        //自定義攔截器
        Map<String, Filter> customFilterMap = new LinkedHashMap<>();
        customFilterMap.put("corsAuthenticationFilter", new CORSAuthenticationFilter());
        shiroFilter.setFilters(customFilterMap);

        return shiroFilter;
    }





    /**
     * securityManager 核心配置
     * 安全控制層
     * @return
     */
    @Bean
    public org.apache.shiro.mgt.SecurityManager securityManager(){
        DefaultWebSecurityManager defaultWebSecurityManager = new DefaultWebSecurityManager();
        //設置自定義的realm
        defaultWebSecurityManager.setRealm(myRealm());
        //自定義的shiro session 緩存管理器
        defaultWebSecurityManager.setSessionManager(sessionManager());
        //將緩存對象注入到SecurityManager中
        defaultWebSecurityManager.setCacheManager(ehCacheManager());

        return defaultWebSecurityManager;
    }


    /**
     * 自定義的realm
     * @return
     */
    @Bean
    public MyRealm myRealm() {
        return new MyRealm();
    }


    /**
     * 開啓shiro 的AOP註解支持
     * @param securityManager
     * @return
     */
    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(org.apache.shiro.mgt.SecurityManager securityManager) {
        AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
        authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
        return authorizationAttributeSourceAdvisor;
    }


    /**
     * shiro緩存管理器
     * 1 添加相關的maven支持
     * 2 註冊這個bean,將緩存的配置文件導入
     * 3 在securityManager 中註冊緩存管理器,以後就不會每次都會去查詢數據庫了,相關的權限和角色會保存在緩存中,但須要注意一點,更新了權限等操做以後,須要及時的清理緩存
     */
    @Bean
    public EhCacheManager ehCacheManager() {
        EhCacheManager cacheManager = new EhCacheManager();
        cacheManager.setCacheManagerConfigFile("classpath:config/ehcache.xml");
        return cacheManager;
    }


    /**
     * 自定義的 shiro session 緩存管理器,用於跨域等狀況下使用 token 進行驗證,不依賴於sessionId
     * @return
     */
    @Bean
    public SessionManager sessionManager(){
        //將咱們繼承後重寫的shiro session 註冊
        ShiroSession shiroSession = new ShiroSession();
        //若是後續考慮多tomcat部署應用,可使用shiro-redis開源插件來作session 的控制,或者nginx 的負載均衡
        shiroSession.setSessionDAO(new EnterpriseCacheSessionDAO());
        return shiroSession;
    }
}

完成了上面的流程,基本就已經大功告成了,對了,還要加上下面的代碼。

package com.common.filter;

import com.alibaba.fastjson.JSONObject;
import com.yunji.kwxt.common.Constant;
import com.yunji.kwxt.common.enums.ResultEnum;
import com.yunji.kwxt.common.model.ResultJson;
import org.apache.shiro.web.filter.authc.FormAuthenticationFilter;

import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.PrintWriter;

/**
 * @author :LX
 * 建立時間: 2019/5/31. 10:25
 * 地點:廣州
 * 目的: 過濾OPTIONS請求
 *      繼承shiro 的form表單過濾器,對 OPTIONS 請求進行過濾。
 *      先後端分離項目中,因爲跨域,會致使複雜請求,即會發送preflighted request,這樣會致使在GET/POST等請求以前會先發一個OPTIONS請求,但OPTIONS請求並不帶shiro
 *      的'authToken'字段(shiro的SessionId),即OPTIONS請求不能經過shiro驗證,會返回未認證的信息。
 *
 * 備註說明: 須要在 shiroConfig 進行註冊
 */
public class CORSAuthenticationFilter extends FormAuthenticationFilter {

    /**
     * 直接過濾能夠訪問的請求類型
     */
    private static final String REQUET_TYPE = "OPTIONS";


    public CORSAuthenticationFilter() {
        super();
    }


    @Override
    public boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
        if (((HttpServletRequest) request).getMethod().toUpperCase().equals(REQUET_TYPE)) {
            return true;
        }
        return super.isAccessAllowed(request, response, mappedValue);
    }


    @Override
    protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
        HttpServletResponse res = (HttpServletResponse)response;
        res.setHeader("Access-Control-Allow-Origin", "*");
        res.setStatus(HttpServletResponse.SC_OK);
        res.setCharacterEncoding("UTF-8");
        PrintWriter writer = res.getWriter();

        ResultJson resultJson = new ResultJson(Constant.ERROR_CODE_NO_LOGIN, ResultEnum.ERROR.getStatus(), "請先登陸系統!", null);
        writer.write(JSONObject.toJSONString(resultJson));
        writer.close();
        return false;
    }
}

爲何要過濾,上面的註釋說的很清楚了,建議你們仍是加上,這個類最終在shiro的攔截器那裏配置了。

固然還有登陸那裏要說一下,不少的新手否則就搞不懂了。

/**
     * 用戶登陸
     * @param username 用戶名
     * @param password 用戶密碼
     * @return
     */
    @RequestMapping(value = "/login", method = RequestMethod.POST)
    @ResponseBody
    public ResultJson login(String username, String password, HttpServletRequest request){

        //TODO 驗證碼驗證
        UsernamePasswordToken token = new UsernamePasswordToken(username, password);
        User user = userService.login(username, password);
        SecurityUtils.getSubject().login(token);

        //更新登陸信息
        user.setIp(HttpTool.getIpAddr(request));
        user.setOs(HttpTool.getOs(request));
        user.setUpdateUserId(user.getId());
        user.setUpdateTime(CommonTool.getTimestamp());

        //設置session時間
        //SecurityUtils.getSubject().getSession().setTimeout(1000*60*30);

        //token信息
        Subject subject = SecurityUtils.getSubject();
        Serializable tokenId = subject.getSession().getId();
        return new ResultJson(null, ResultEnum.SUCCESS.getStatus(), "登陸認證成功", tokenId);
    }

咱們最終從shiro的session中取到了sessionId,回傳給前端,前端後續的請求都要帶這個token。

這樣就實現了token方式的shiro整合springboot。

若是爲了安全,還能夠建議你們,獲取到sessionId 以後,咱們進行一次加密,而後返回給前端,前端返回給咱們的時候,咱們能夠在shirosession 類中對加密的sessionId解密,這樣就更安全了。

最後還有一個問題須要說明一下,上述的代碼中shiro使用了緩存,但個人緩存相關的配置卻沒有貼出來,由於我這裏用的是java的緩存框架,建議使用redis的緩存框架,若是使用了緩存框架,細心的小夥伴就會發現,若是登陸後,在必定時間沒有和後臺進行交互,這個sessionId就會失效。

這是由於,當咱們登陸後若是走了緩存,session的存活時間就被緩存管理起來,咱們即便設置了shiro的緩存時間,設置應用的緩存時間都沒法管理到第三方的緩存,shiro的sesssion和server的session不是同一個東西。他並非servlet來管理的,故而設置了也沒有做用,須要去設置緩存中這個對象存活時間纔有用,好比咱們弄了redis來管理sessionId,只有設置了在redis中session的存活時間才行,咱們直接設置

SecurityUtils.getSubject().getSession().setTimeout(1000*60*30);
#session過時時間,單位秒
server.servlet.session.timeout=30000

都沒有任何用。好比我上面用ehcache來管理緩存,那只有在該緩存框架中設置這個參數纔有用

我這裏設置了120S,那若是120S沒有任何交互,那這個緩存sessionId就會失效

相關文章
相關標籤/搜索