Shrio使用Jwt達到前後端分離

来源:https://www.cnblogs.com/fzsyw/archive/2019/08/24/11405504.html

概述 前後端分離之後,因為HTTP本身是無狀態的,Session就沒法用了。項目採用jwt的方案後,請求的主要流程如下:用戶登錄成功之後,服務端會創建一個jwt的token(jwt的這個token中記錄了當前的操作賬號),並將這個token返回給前端,前端每次請求服務端的數據時,都會將令牌放入Hea ...


概述

前後端分離之後,因為HTTP本身是無狀態的,Session就沒法用了。項目採用jwt的方案後,請求的主要流程如下:用戶登錄成功之後,服務端會創建一個jwt的token(jwt的這個token中記錄了當前的操作賬號),並將這個token返回給前端,前端每次請求服務端的數據時,都會將令牌放入Header或者Parameter中,服務端接收到請求後,會先被攔截器攔截,token檢驗的攔截器會獲取請求中的token,然後會檢驗token的有效性,攔截器都檢驗成功後,請求會成功到達實際的業務流程中,執行業務邏輯返回給前端數據。在這個過程中,主要涉及到Shiro的攔截器鏈,Jwt的token管理,多Realm配置等。

Shiro的Filter鏈

Shiro的認證和授權都離不開Filter,因此需要對Shiro的Filter的運行流程很清楚,才能自定義Filter來滿足企業的實際需要。另外Shiro的Filter雖然原理都和Servlet的Filter相似,甚至都最終繼承相同的介面,但是實際還是有些差別。Shiro中的Filter主要是在ShiroFilter內,對指定匹配的URL進行攔截處理,它有自己的Filter鏈;而Servlet的Filter和ShiroFilter是同一個級別的,即先走Shiro自己的Filter體系,然後才會委托給Servlet容器的FilterChain進行Servlet容器級別的Filter鏈執行

分析Shiro的預設Filter

在Shiro和Spring Boot整合過程中,需要配置ShiroFilterFactoryBean,該類是ShiroFilter的工廠類,並繼承了FactoryBean介面。可以從該介面的方法來分析。該介面getObject獲取一個實例,按照邏輯,發現調用createFilterChainManager,並創建預設的Filter(按照命名猜測Map<String, Filter> defaultFilters = manager.getFilters())。

public class ShiroFilterFactoryBean implements FactoryBean, BeanPostProcessor {
    private Map<String, Filter> filters;

    private Map<String, String> filterChainDefinitionMap; 

    /**
     *
     * 該工廠類生產的產品類
     */
    public Object getObject() throws Exception {
        if (instance == null) {
            instance = createInstance();
        }
        return instance;
    }

    protected FilterChainManager createFilterChainManager() {
        //創建預設Filter
        DefaultFilterChainManager manager = new DefaultFilterChainManager();
        Map<String, Filter> defaultFilters = manager.getFilters();
        for (Filter filter : defaultFilters.values()) {
            applyGlobalPropertiesIfNecessary(filter);
        }

        Map<String, Filter> filters = getFilters();
        if (!CollectionUtils.isEmpty(filters)) {
            for (Map.Entry<String, Filter> entry : filters.entrySet()) {
                String name = entry.getKey();
                Filter filter = entry.getValue();
                applyGlobalPropertiesIfNecessary(filter);
                if (filter instanceof Nameable) {
                    ((Nameable) filter).setName(name);
                }
                manager.addFilter(name, filter, false);
            }
        }

        Map<String, String> chains = getFilterChainDefinitionMap();
        if (!CollectionUtils.isEmpty(chains)) {
            for (Map.Entry<String, String> entry : chains.entrySet()) {
                String url = entry.getKey();
                String chainDefinition = entry.getValue();
                manager.createChain(url, chainDefinition);
            }
        }

        return manager;
    }

    protected AbstractShiroFilter createInstance() throws Exception {

        log.debug("Creating Shiro Filter instance.");

        SecurityManager securityManager = getSecurityManager();
        if (securityManager == null) {
            String msg = "SecurityManager property must be set.";
            throw new BeanInitializationException(msg);
        }

        if (!(securityManager instanceof WebSecurityManager)) {
            String msg = "The security manager does not implement the WebSecurityManager interface.";
            throw new BeanInitializationException(msg);
        }
        //創建FilterChainManager
        FilterChainManager manager = createFilterChainManager();

        PathMatchingFilterChainResolver chainResolver = new PathMatchingFilterChainResolver();
        chainResolver.setFilterChainManager(manager);

        return new SpringShiroFilter((WebSecurityManager) securityManager, chainResolver);
    }
    
   ...
}

DefaultFilterChainManageraddDefaultFilters來添加預設的Filter,DefaultFilter為一系列預設Filter的枚舉類。

public class DefaultFilterChainManager implements FilterChainManager {
    
    public Map<String, Filter> getFilters() {
        return filters;
    }

    protected void addFilter(String name, Filter filter, boolean init, boolean overwrite) {
        Filter existing = getFilter(name);
        if (existing == null || overwrite) {
            if (filter instanceof Nameable) {
                ((Nameable) filter).setName(name);
            }
            if (init) {
                initFilter(filter);
            }
            this.filters.put(name, filter);
        }
    }

     /**
     *
     * 創建預設的Filter
     */
    protected void addDefaultFilters(boolean init) {
        for (DefaultFilter defaultFilter : DefaultFilter.values()) {
            addFilter(defaultFilter.name(), defaultFilter.newInstance(), init, false);
        }
    }
    ...
}

從這個枚舉類中可以看到之前添加的共有11個預設Filter,它們的名字分別是anon,authc,authcBaisc等。

public enum DefaultFilter {

    anon(AnonymousFilter.class),
    authc(FormAuthenticationFilter.class),
    authcBasic(BasicHttpAuthenticationFilter.class),
    logout(LogoutFilter.class),
    noSessionCreation(NoSessionCreationFilter.class),
    perms(PermissionsAuthorizationFilter.class),
    port(PortFilter.class),
    rest(HttpMethodPermissionFilter.class),
    roles(RolesAuthorizationFilter.class),
    ssl(SslFilter.class),
    user(UserFilter.class);

    private final Class<? extends Filter> filterClass;

    private DefaultFilter(Class<? extends Filter> filterClass) {
        this.filterClass = filterClass;
    }

    public Filter newInstance() {
        return (Filter) ClassUtils.newInstance(this.filterClass);
    }

    public Class<? extends Filter> getFilterClass() {
        return this.filterClass;
    }
    ...
}

Filter的繼承體系分析

  • NameableFilter給Filter起個名字,如果沒有設置,預設名字就是FilterName。

  • OncePerRequestFilter用於防止多次執行Filter;也就是說一次請求只會走一次攔截器鏈;另外提供 enabled 屬性,表示是否開啟該攔截器實例,預設 enabled=true 表示開啟,如果不想讓某個攔截器工作,可以設置為 false 即可。

  • AdviceFilter提供了AOP風格的支持。preHandler:在攔截器鏈執行之前執行,如果返回true則繼續攔截器鏈;否則中斷後續的攔截器鏈的執行直接返回;可以進行預處理(如身份驗證、授權等行為)。postHandle:在攔截器鏈執行完成後執行,後置處理(如記錄執行時間之類的)。afterCompletion:類似於AOP中的後置最終增強;即不管有沒有異常都會執行,可以進行清理資源(如接觸 Subject 與線程的綁定之類的)。

  • PathMatchingFilter內置了pathMatcher的實例,方便對請求路徑匹配功能及攔截器參數解析的功能,如下所示,對匹配的路徑執行isFilterChainContinued的邏輯,如果都沒配到,則直接交給攔截器鏈。

protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {

    if (this.appliedPaths == null || this.appliedPaths.isEmpty()) {
        if (log.isTraceEnabled()) {
            log.trace("appliedPaths property is null or empty.  This Filter will passthrough immediately.");
        }
        return true;
    }

    for (String path : this.appliedPaths.keySet()) {
        //對匹配路徑進行處理
        if (pathsMatch(path, request)) {
            log.trace("Current requestURI matches pattern '{}'.  Determining filter chain execution...", path);
            Object config = this.appliedPaths.get(path);
            return isFilterChainContinued(request, response, path, config);
        }
    }

    return true;
}
  • AccessControlFilter提供了訪問控制的基礎功能,isAccessAllowed訪問通過,則交給攔截器鏈,不通過則執行onAccessDenied來確定交給攔截器還是自己處理
 public boolean onPreHandle(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception {
        return isAccessAllowed(request, response, mappedValue) || onAccessDenied(request, response, mappedValue);
    }
  • AuthenticationFilter認證Filter的基類,一般在isAccessAllowed中執行認證邏輯,另外該Filter提供登錄成功後跳轉的功能
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object      mappedValue) {
    Subject subject = getSubject(request, response);
    return subject.isAuthenticated();
}


protected void issueSuccessRedirect(ServletRequest request, ServletResponse response) throws        Exception {
    WebUtils.redirectToSavedRequest(request, response, getSuccessUrl());
}
  • AuthenticatingFilter是AuthenticationFilter的子類,提供了executeLogin通用邏輯,通常由子類來實現protected abstract AuthenticationToken createToken(ServletRequest request, ServletResponse response)該方法,然後執行subject.login(token)
public abstract class AuthenticatingFilter extends AuthenticationFilter {
    public static final String PERMISSIVE = "permissive";

    protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception {
        AuthenticationToken token = createToken(request, response);
        if (token == null) {
            String msg = "createToken method implementation returned null. A valid non-null AuthenticationToken " +
                "must be created in order to execute a login attempt.";
            throw new IllegalStateException(msg);
        }
        try {
            Subject subject = getSubject(request, response);
            subject.login(token);
            return onLoginSuccess(token, subject, request, response);
        } catch (AuthenticationException e) {
            return onLoginFailure(token, e, request, response);
        }
    }

    protected abstract AuthenticationToken createToken(ServletRequest request, ServletResponse response) throws Exception;

    protected AuthenticationToken createToken(String username, String password,
                                              ServletRequest request, ServletResponse response) {
        boolean rememberMe = isRememberMe(request);
        String host = getHost(request);
        return createToken(username, password, rememberMe, host);
    }

    protected AuthenticationToken createToken(String username, String password,
                                              boolean rememberMe, String host) {
        return new UsernamePasswordToken(username, password, rememberMe, host);
    }

    protected boolean onLoginSuccess(AuthenticationToken token, Subject subject,
                                     ServletRequest request, ServletResponse response) throws Exception {
        return true;
    }

    protected boolean onLoginFailure(AuthenticationToken token, AuthenticationException e,
                                     ServletRequest request, ServletResponse response) {
        return false;
    }

    @Override
    protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
        return super.isAccessAllowed(request, response, mappedValue) ||
            (!isLoginRequest(request, response) && isPermissive(mappedValue));
    }
    ...
}

在Shiro中添加自定義的Filter

從上面源碼分析,知道了Shiro會提供11個預設的Filter,也是按照攔截器模式交由FilterChainManager來管理Filter,並最終返回SpringShiroFilter。所以添加自定義的Filter,主要有三步。

  • 實現自己的Filter

如下實現了自己的JwtFilter,主要邏輯可以參考FormAuthenticationFilter。JwtFilter主要是對前端的Api進行校驗,檢驗失敗,則拋出異常信息,不給攔截器鏈處理。

@Slf4j
public class JwtFilter extends AuthenticatingFilter {   
    private static final String TOKEN_NAME = "token";
    
    /**
     * 創建令牌
     */
    @Override
    protected AuthenticationToken createToken(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception {
        final String token = getToken((HttpServletRequest) servletRequest);     
        if(StringUtils.isEmpty(token)) {
            return null;
        }       
        return new JwtToken(token);     
    }
    
    /**
     * 獲取令牌
     * @param httpServletRequest
     * @return
     */
    private String getToken(HttpServletRequest httpServletRequest) {
        String token = httpServletRequest.getHeader(TOKEN_NAME);
        if(StringUtils.isEmpty(token)) {
            token = httpServletRequest.getParameter(TOKEN_NAME);
        };
        if(StringUtils.isEmpty(token)) {
            Cookie[] cookies = httpServletRequest.getCookies();
            if(ArrayUtils.isNotEmpty(cookies)) {
                for(Cookie cookie :cookies) {
                    if(TOKEN_NAME.equals(cookie.getName())) {
                        token = cookie.getValue();
                        break;
                    }
                }
            }
        };  
        return token;
    }
 
    /**
     * 未通過處理
     * @param servletRequest
     * @param servletResponse
     * @return
     * @throws Exception
     */
    @Override
    protected boolean onAccessDenied(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception {
        return executeLogin(servletRequest, servletResponse);
    }

    /**
     * 登錄失敗執行方法
     * @param token
     * @param e
     * @param request
     * @param response
     * @return
     */
    protected boolean onLoginFailure(AuthenticationToken token, AuthenticationException e, ServletRequest request,
            ServletResponse response) {
        response.setContentType("text/html;charset=UTF-8");
        try(OutputStream outputStream = response.getOutputStream()){
            outputStream.write(e.getMessage().getBytes(SystemConsts.CHARSET));
            outputStream.flush();           
        } catch (IOException e1) {
            e1.printStackTrace();
        }   
        return false;
    }
    ...
}
  • 將Filter添加到Shiro中

將自定義的Filter添加到Shiro,並要指定的匹配路徑。

public ShiroFilterFactoryBean getShiroFilterFactoryBean(@Autowired          org.apache.shiro.mgt.SecurityManager securityManager, @Autowired JwtFilter jwtFilter) {
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();

        Map<String, Filter> filterMap = new LinkedHashMap<>();
        filterMap.put("jwt", jwtFilter);
        shiroFilterFactoryBean.setFilters(filterMap);
    
        Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
        filterChainDefinitionMap.put("/**", "jwt"); 
        shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
    ...
        return shiroFilterFactoryBean;
    }

註意:SpringBoot自動幫我們註冊了我們的Filter(Filter是註冊到整個Filter鏈,而不是Shiro的Filter鏈),但是在Shiro中,我們需要自己實現註冊,但是又需要Filter實例存在於Spring容器中,以便能使用其他眾多服務(自動註入其他組件……)。所以需要取消Spring Boot的自動註入Filter。可以採用如下方式:

@Bean
public FilterRegistrationBean registration(@Qualifier("devCryptoFilter") DevCryptoFilter filter){
    FilterRegistrationBean registration = new FilterRegistrationBean(filter);
    registration.setEnabled(false);
    return registration;
}

Jwt整合

使用Jwt需要我們提供對token的創建,校驗和獲取token中信息的方法。網上有很多,可以借鑒,而且token中也可以存一些其他數據。

public class JwtUtil {

    /**
     * 檢驗token
     * @return boolean
     */
    public static boolean verify(String token, String username) {
        ...
    }

    /**
     * 獲得token中的屬性
     * @return token中包含的屬性
     */
    public static String getValue(String token, String key) {
        ...
    }

    /**
     * 生成token簽名EXPIRE_TIME 分鐘後過期
     * 
     * @param username
     *            用戶名
     * @return 加密的token
     */
    public static String createJWT(String userId) {
        ...
    }
}

多Realm配置

用戶密碼認證和Jwt的認證需要不同的兩個Realm,多Realm需要處理不同的Realm,獲取到指定Realm的AuthenticationToken的數據模型。

  • 實現ModularRealmAuthenticator的方法
public class MultiRealmAuthenticator extends ModularRealmAuthenticator {

    @Override
    protected AuthenticationInfo doAuthenticate(AuthenticationToken authenticationToken) 
            throws AuthenticationException {
        assertRealmsConfigured();
        
        List<Realm> realms = this.getRealms()
                .stream()
                .filter(realm -> {
                    return realm.supports(authenticationToken);
                })
                .collect(Collectors.toList());
        
        return realms.size() == 1 ? this.doSingleRealmAuthentication(realms.get(0), authenticationToken) : 
            this.doMultiRealmAuthentication(realms, authenticationToken);
    }
}
  • AuthenticatingRealm中實現getAuthenticationTokenClass方法
public Class getAuthenticationTokenClass() {
    return JwtToken.class;
}
  • 在SecurityManager中配置
@Bean(name = "securityManager")
public org.apache.shiro.mgt.SecurityManager defaultWebSecurityManager(@Autowired UserRealm      userRealm,  @Autowired TokenRealm tokenValidateRealm) {
    securityManager.setAuthenticator(multiRealmAuthenticator());
    securityManager.setRealms(Arrays.asList(userRealm, tokenValidateRealm));
    ...
    return securityManager;
}

整合Swagger

添加Swagger依賴

<dependency>
    <groupId>io.springfox</groupId>
    <artifactId>springfox-swagger2</artifactId>
    <version>2.9.2</version>
</dependency>

<dependency>
    <groupId>io.springfox</groupId>
    <artifactId>springfox-swagger-ui</artifactId>
    <version>2.9.2</version>
</dependency>

添加Swagger的配置

@Configuration
public class Swagger2Config {

    @Bean
    public Docket createRestApi() {
        return new Docket(DocumentationType.SWAGGER_2)
                .apiInfo(apiInfo())
                .select()
                .apis(RequestHandlerSelectors.basePackage("XXX"))
                .paths(PathSelectors.any())
                .build();
    }

    private ApiInfo apiInfo() {
        return new ApiInfoBuilder()
                .title("XXX")
                .description("經供參考")
                .version("1.0")
                .build();
    }
}

總結

在整個過程中,遇到的坑就是在Spring boot中Filter的自動註入,中間考慮有不使用註入的方式解決,即直接使用new JwtFilter()的方式,雖然也能解決問題,但是不是很完美,最終還是在網上找到解決方案。對Shiro的Filter鏈的執行過程加強了理解,能夠使用自定的Filter解決實際問題。還有一個後續的問題,退出登錄時的Jwt的token處理,它本身不能像Session一樣,退出就清除,理論上只要沒過期,就一直存在。可以考慮使用緩存,退出時清除即可,然後在校驗時,先從緩存獲取進行判斷。


您的分享是我們最大的動力!

更多相關文章
  • 1. course 1.進程創建的兩種方式 1. 開啟進程的第一種方式: 2. 開啟進程的第二種方式: 3. 簡單應用 2.獲取進程pid 3.驗證進程之間的空間隔離 4. join 5.進程的其他參數 6.守護進程 7.僵屍進程孤兒進程 基於 環境( ) 主進程需要等待子進程結束之後,主進程才結束 ...
  • 服務鏈路跟蹤 背景 微服務以微出名,在實際的開發過程中,涉及到成百上千個服務,網路請求引起服務之間的調用極其複雜。 當請求不可用或者變慢時,需要及時排查出故障服務點成為了微服務維護的一大難關。 服務鏈路跟蹤技術應運而生。 ZipKin Zipkin 是一個開放源代碼分散式的跟蹤系統,由Twitter ...
  • Python 入門之常用運算符 算數運算符 比較運算符 賦值運算符 邏輯運算符 成員運算符 位運算符 身份運算符 Python運算符優先順序 ...
  • HTTP不能保持連接,可使用會話保存用戶信息。 常用的會話技術有2種:Cookie、Session。 Cookie 1、原理 當用戶第一次訪問某個網站時,伺服器設置Cookie,存儲用戶信息,放在響應頭欄位中,隨HTTP響應傳給瀏覽器,瀏覽器把Cookie存儲到本地電腦上。 當用戶再次訪問該網站時 ...
  • 摘要: 在 的版本變遷過程中,註解發生了很多的變化,然而代理的設計也發生了微妙的變化,從 的`ProxyFactoryBean Spring2.x Aspectj`註解,最後到了現在廣為熟知的自動代理。 說明: 代理的相關配置類 實現了 ,封裝了對 和`Advisor`的操作 該類及其子類主要是利用 ...
  • django搭建BBS 表單創建&註冊 0824自我總結 文件結構 app 介面 migrations _\_inint\_\_.py admin.py apps.py bbsform.py models.py tests.py views.py avatar BBS \_\_inint\_\_.p ...
  • 1. Lambda簡介 可以把Lambda表達式理解為簡潔地表示可傳遞的匿名函數的一種方式:它沒有名稱,但它有參數列表、函數主體、返回類型,可能還有一個可以拋出的異常列表。 匿名——我們說匿名,是因為它不像普通的方法那樣有一個明確的名稱:寫得少而想得多! 函數——我們說它是函數,是因為Lambda函 ...
  • 在設計Web應用程式的時候,經常需要把一個系統進行結構化設計,即按照模塊進行劃分,讓不同的Servlet來實現不同的功能,例如可以讓其中一個Servlet接收用戶的請求,另外一個Servlet來處理用戶的請求。為了實現這種程式的模塊化,就需要保證在不同的Servlet之間可以相互跳轉,而Servle ...
一周排行
  • 1. UWP中的其它裁剪方案 之前在 "這篇文章" 里,我介紹瞭如何使用 "UIElement.Clip" 裁剪UIElement的內容,使用代碼如下: 在 "另一篇文章里" 我介紹瞭如何使用 "CanvasActiveLayer" 裁剪Win2D內容,使用代碼如下: 這兩種方式都有他們的局限:Ca ...
  • 前言 軟體開發過程中,不可避免會用到集合,C#中的集合表現為數組和若幹集合類。不管是數組還是集合類,它們都有各自的優缺點。如何使用好集合是我們在開發過程中必須掌握的技巧。不要小看這些技巧,一旦在開發中使用了錯誤的集合或針對集合的方法,應用程式將會背離你的預想而運行。 本文已更新至http://www ...
  • 建議20、使用泛型集合來替代非泛型集合 http://www.cnblogs.com/aehyok/p/3384637.html 這裡有一篇文章,是我之前專門來介紹泛型的。我們應儘量的使用泛型集合。因為泛型的確有它的好處: 1、提供了類型安全,在編譯期間就可以檢查錯誤 2、更重要的是大部分情況下泛型 ...
  • 常見面試題目: 1. 值類型和引用類型的區別? 2. 結構和類的區別? 3. delegate是引用類型還是值類型?enum、int[]和string呢? 4. 堆和棧的區別? 5. 什麼情況下會在堆(棧)上分配數據?它們有性能上的區別嗎? 6.“結構”對象可能分配在堆上嗎?什麼情況下會發生,有什麼 ...
  • 5年,50+版本迭代,不忘初心,始終為了幫助廣大站長更容易實現站內搜索功能 一,開發初衷與思路: 基於Lucene.net重頭開發一個站內搜索其實挺費時,且需要一定的二開能力,很多人直接組合一些第三方開源的組件,但如果需要解決好搜索質量,兼顧搜準率與召回率,依然需要做不少調整優化的工作,後來我想能否 ...
  • Centos+Nginx+NetCore3.1部署 1 先將vs2019升級到,16.4.0版本 2.新建一.netcore3.1的web程式 3.編譯後將項目上傳到centos伺服器 4.配置nginx文件 修改完後重新載入ngxin. service nginx restart 5.安裝守護進程 ...
  • 1. DateTime的使用 DateTime dt = DateTime.Now; StringBuilder sb = new StringBuilder(); sb.AppendLine(string.Format("DataTime字元串表示: {0}", dt.ToString())); ...
  • 此系列使用Asp.net構建前後端分離的博客網站。 創建一個asp.net項目 我們這裡使用的是空模板,把Https配置去掉(安全先不配置) 構建webapi介面有很多方法,在這裡我們選擇最簡單的2種方式進行搭建。 1.WebForm 創建一個webForm 打開窗體的伺服器邏輯代碼文件 添加如下方 ...
  • 這是 站點的源代碼 源碼 框架組件 jQuery + Bootstrap4 .NET Core (latest) EF + Linq 支持:SQLServer、MySQL、PostgreSQL、SQLite、InMemory等 ==================================== ...
  • Netnr.FileServer 基於 .NET Core 的簡單文件伺服器,資料庫為SQLite 源碼 https://github.com/netnr/blog https://gitee.com/netnr/blog 為了方便維護公共類庫,項目放置一個解決方案,沒有分開 配置 為配置文件 正式 ...
x