Shrio使用Jwt達到前後端分離
概述
前後端分離之後,因為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); } ... }
在DefaultFilterChainManager
中addDefaultFilters
來新增預設的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一樣,退出就清除,理論上只要沒過期,就一直存在。可以考慮使用快取,退出時清除即可,然後在校驗時,先從快取獲取進行判斷