先後端分離以後,由於HTTP自己是無狀態的,Session就無法用了。項目採用jwt的方案後,請求的主要流程以下:用戶登陸成功以後,服務端會建立一個jwt的token(jwt的這個token中記錄了當前的操做帳號),並將這個token返回給前端,前端每次請求服務端的數據時,都會將令牌放入Header或者Parameter中,服務端接收到請求後,會先被攔截器攔截,token檢驗的攔截器會獲取請求中的token,而後會檢驗token的有效性,攔截器都檢驗成功後,請求會成功到達實際的業務流程中,執行業務邏輯返回給前端數據。在這個過程當中,主要涉及到Shiro的攔截器鏈,Jwt的token管理,多Realm配置等。javascript
Shiro的認證和受權都離不開Filter,所以須要對Shiro的Filter的運行流程很清楚,才能自定義Filter來知足企業的實際須要。另外Shiro的Filter雖然原理都和Servlet的Filter類似,甚至都最終繼承相同的接口,可是實際仍是有些差異。Shiro中的Filter主要是在ShiroFilter內,對指定匹配的URL進行攔截處理,它有本身的Filter鏈;而Servlet的Filter和ShiroFilter是同一個級別的,即先走Shiro本身的Filter體系,而後纔會委託給Servlet容器的FilterChain進行Servlet容器級別的Filter鏈執行html
在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); } ... }
在DefaultFilterChainManager
中addDefaultFilters
來添加默認的Filter,DefaultFilter爲一系列默認Filter的枚舉類。java
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等。spring
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; } ... }
NameableFilter給Filter起個名字,若是沒有設置,默認名字就是FilterName。apache
OncePerRequestFilter用於防止屢次執行Filter;也就是說一次請求只會走一次攔截器鏈;另外提供 enabled 屬性,表示是否開啓該攔截器實例,默認 enabled=true 表示開啓,若是不想讓某個攔截器工做,能夠設置爲 false 便可。後端
AdviceFilter提供了AOP風格的支持。preHandler:在攔截器鏈執行以前執行,若是返回true則繼續攔截器鏈;不然中斷後續的攔截器鏈的執行直接返回;能夠進行預處理(如身份驗證、受權等行爲)。postHandle:在攔截器鏈執行完成後執行,後置處理(如記錄執行時間之類的)。afterCompletion:相似於AOP中的後置最終加強;即無論有沒有異常都會執行,能夠進行清理資源(如接觸 Subject 與線程的綁定之類的)。api
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; }
onAccessDenied
來肯定交給攔截器仍是本身處理public boolean onPreHandle(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception { return isAccessAllowed(request, response, mappedValue) || onAccessDenied(request, response, mappedValue); }
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()); }
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會提供11個默認的Filter,也是按照攔截器模式交由FilterChainManager來管理Filter,並最終返回SpringShiroFilter。因此添加自定義的Filter,主要有三步。cookie
以下實現了本身的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,並要指定的匹配路徑。
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須要咱們提供對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) { ... } }
用戶密碼認證和Jwt的認證須要不一樣的兩個Realm,多Realm須要處理不一樣的Realm,獲取到指定Realm的AuthenticationToken的數據模型。
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); } }
getAuthenticationTokenClass
方法public Class getAuthenticationTokenClass() { return JwtToken.class; }
@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; }
<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>
@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同樣,退出就清除,理論上只要沒過時,就一直存在。能夠考慮使用緩存,退出時清除便可,而後在校驗時,先從緩存獲取進行判斷。