由于手機(jī)端不能存cookie,所以傳統(tǒng)的session存儲(chǔ)登錄信息的登錄方式(后面簡(jiǎn)稱session登錄)不能用冠息,所以需要一個(gè)既支持session登錄后訪問(wèn)有訪問(wèn)權(quán)限控制的url又支持無(wú)狀態(tài)化token方式的認(rèn)證。對(duì)于無(wú)狀態(tài)話的token認(rèn)證,目前比較流行的是JWT token。關(guān)于JWT Token的介紹請(qǐng)自行查閱網(wǎng)上資料戈擒。由于我們使用的Shiro認(rèn)證授權(quán)框架,Shiro默認(rèn)實(shí)現(xiàn)的是基于Session的認(rèn)證和授權(quán)艰毒,為了實(shí)現(xiàn)同時(shí)支持Session和JWT Token兩種認(rèn)證方式筐高,需要在了解Shiro認(rèn)證授權(quán)框架的集成上 實(shí)現(xiàn)JWT token的訪問(wèn)控制邏輯。
1. 認(rèn)證流程
針對(duì)用戶需求和安全需求丑瞧,需要實(shí)現(xiàn)以下幾種場(chǎng)景的認(rèn)證柑土。
- 基于瀏覽器的Session認(rèn)證方式,需要實(shí)現(xiàn)多個(gè)web應(yīng)用之間的SSO绊汹。
- 移動(dòng)端基于JWT Token的無(wú)狀態(tài)認(rèn)證稽屏,需要考慮token的足夠安全和token的自動(dòng)刷新(因?yàn)橐苿?dòng)端不能因?yàn)閠oken的過(guò)期,而中斷應(yīng)用導(dǎo)致用戶體驗(yàn)差)
- 由前端發(fā)起西乖,后端微服務(wù)之間的調(diào)用狐榔,由于這種調(diào)用關(guān)系,微服務(wù)之間會(huì)進(jìn)行session的共享获雕,可以通過(guò)cookie來(lái)實(shí)現(xiàn)SSO
-
來(lái)自于內(nèi)部的一些服務(wù)薄腻,比如定時(shí)的Point service,由于它無(wú)Session典鸡,因此對(duì)于這種服務(wù)被廓,系統(tǒng)會(huì)內(nèi)置一個(gè)系統(tǒng)用戶,再以JWT Token的方式進(jìn)行認(rèn)證
Screen Shot 2021-08-03 at 3.41.22 PM.png
上面紅色連接線表示基于JWT Token的Mobile App認(rèn)證方式萝玷,藍(lán)色連線表示基于Session的登錄方式嫁乘。其中內(nèi)部定時(shí)器或者服務(wù)也是基于JWT Token認(rèn)證方式,只是需要內(nèi)置一些系統(tǒng)用戶球碉。
2. 實(shí)現(xiàn)步驟
2.1. Shiro默認(rèn)訪問(wèn)步驟
場(chǎng)景一蜓斧、訪問(wèn)登錄請(qǐng)求
比如我們常見(jiàn)會(huì)定義一個(gè)/login的請(qǐng)求,接受用戶名和密碼參數(shù)(一般密碼都會(huì)加鹽hash)睁冬。對(duì)于這種請(qǐng)求挎春,Shiro會(huì)執(zhí)行以下的兩步邏輯。
- 在代碼里會(huì)寫(xiě)到獲取Shiro的Subject豆拨,創(chuàng)建一個(gè)token直奋,通常是UsernamePasswordToken,將請(qǐng)求參數(shù)的賬戶密碼填充進(jìn)去施禾,然后調(diào)用subject.login(token)
- 接下來(lái)到支持處理這個(gè)token的realm中調(diào)用 realm doGetAuthenticationInfo 鑒權(quán)脚线,鑒權(quán)后,session中就存有你的登錄信息了
場(chǎng)景二弥搞、訪問(wèn)普通API
- 到 Shiro 的 PathMatchingFilter preHandle 方法判斷一個(gè)請(qǐng)求的訪問(wèn)權(quán)限是可以直接放行還是需要 Shiro 自己實(shí)現(xiàn)的AccessControlFilter 來(lái)處理訪問(wèn)請(qǐng)求
- 假設(shè)到了 AccessControlFilter 實(shí)現(xiàn)類邮绿,首先在 isAccessAllowed 判斷是否可以訪問(wèn),如果可以則直接放行訪問(wèn),如果不可以則到 onAccessDenied 方法處理,并繼續(xù)調(diào)用 realm doGetAuthorizationInfo 授權(quán)判斷是否有足夠的權(quán)限來(lái)訪問(wèn)
- 假設(shè)有足夠的權(quán)限的話就訪問(wèn)到自己定義的 controller了
2.2. 支持JWT Token訪問(wèn)
Shiro默認(rèn)支持的是Session認(rèn)證方式坤邪,為了支持JWT Token認(rèn)證方式炫七,需要實(shí)現(xiàn) AccessControlFilter 來(lái)修改控制訪問(wèn)的邏輯。需要完成的工作有以下方面:
要做的有下面幾方面
- [自定義實(shí)現(xiàn)AccessControlFilter (JWTAuthcFilter)]
- S[hiro的過(guò)濾鏈上添加自定義的]
- [自定義realm(JWTShiroRealm][)啃洋,不用賬戶密碼登錄鑒權(quán)(UsernamePasswordToken),而使用自定義的token(JWTToken]
- [自定義一個(gè)token(TokenRealm),存儲(chǔ)參數(shù)和加密參數(shù)等]
- 增加一個(gè)JWTTokenRefreshInterceptor來(lái)攔截請(qǐng)求酱鸭,檢測(cè)是否需要刷新token
2.3. 實(shí)現(xiàn)詳情
具體見(jiàn)代碼,分別是JWTAuthcFilter加袋,JWTPrincipal凛辣,JWTTokenRefreshInterceptor,JWTWebMvcConfigurer职烧,ShiroConfig扁誓,JWTToken等。
2.3.1 JWTAuthcFilter
import com.google.common.base.Strings;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.web.filter.AccessControlFilter;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@Slf4j
@AllArgsConstructor
public class JWTAuthcFilter extends AccessControlFilter {
private final String headerKeyOfToken;
private final JWTUserAuthService userAuthService;
private final boolean isDisabled;
@Override
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
if(isDisabled){
log.info("Shiro Authentication is disabled, hence can access api directly.");
return true;
}else{
log.info("Shiro Authentication is enabled, to continue to execute onAccessDenied method");
}
return false;
}
@Override
protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws IOException {
// 登錄狀態(tài)判斷
log.info("onAccessDenied......");
Subject subject = getSubject(request, response);
if (subject.isAuthenticated()) {
return true;
}
//從header或URL參數(shù)中查找token
HttpServletRequest req = (HttpServletRequest) request;
String authorization = req.getHeader(headerKeyOfToken);
if (Strings.isNullOrEmpty(authorization)) {
authorization = req.getParameter(headerKeyOfToken);
}
JWTToken token = new JWTToken(authorization);
try {
getSubject(request, response).login(token);
} catch (Exception e) {
log.error("認(rèn)證失敗:" + e.getMessage());
this.userAuthService.onAuthenticationFailed((HttpServletRequest) request, (HttpServletResponse) response);
return false;
}
return true;
}
}
2.3.2 JWTPrincipal
import lombok.Data;
@Data
public class JWTPrincipal {
private String account;
private int userId;
private long expiresAt;
}
2.3.3 JWTWebMvcConfigurer
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.InterceptorRegistration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Slf4j
@Configuration
@ConditionalOnProperty(prefix = "shiro.jwt", name = "enable-auto-refresh-token", havingValue = "true")
public class JWTWebMvcConfigurer implements WebMvcConfigurer {
@Autowired
private ShiroConfig shiroConfig;
@Autowired
private JWTUserAuthService userAuthService;
@Bean
@ConditionalOnProperty(prefix = "shiro.jwt", name = "enable-auto-refresh-token", havingValue = "true")
public JWTTokenRefreshInterceptor tokenRefreshInterceptor() {
return new JWTTokenRefreshInterceptor(userAuthService, shiroConfig.getHeaderKeyOfToken(),
shiroConfig.getMaxAliveMinute(), shiroConfig.getAccountAlias());
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
InterceptorRegistration reg = registry.addInterceptor(tokenRefreshInterceptor());
String[] patterns = shiroConfig.getUrlPattern().split(",");
log.info("啟用token自動(dòng)刷新機(jī)制蚀之,已注冊(cè)TokenRefreshInterceptor");
for (String urlPattern : patterns) {
log.info("TokenRefreshInterceptor匹配URL規(guī)則:" + urlPattern);
reg.addPathPatterns(urlPattern);
}
}
@Override
public void addCorsMappings(CorsRegistry registry) {
//允許訪問(wèn)header中的與token相關(guān)屬性
String[] urls = shiroConfig.getUrlPattern().split(",");
for (String url : urls) {
registry.addMapping(url).exposedHeaders(shiroConfig.getHeaderKeyOfToken());
}
}
}
2.3.4 ShiroConfig
import cn.hutool.core.codec.Base64;
import cn.hutool.core.util.StrUtil;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.authc.credential.CredentialsMatcher;
import org.apache.shiro.authc.pam.FirstSuccessfulStrategy;
import org.apache.shiro.authc.pam.ModularRealmAuthenticator;
import org.apache.shiro.cache.ehcache.EhCacheManager;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.realm.Realm;
import org.apache.shiro.session.mgt.eis.EnterpriseCacheSessionDAO;
import org.apache.shiro.session.mgt.eis.SessionDAO;
import org.apache.shiro.spring.LifecycleBeanPostProcessor;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.CookieRememberMeManager;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.apache.shiro.web.servlet.Cookie;
import org.apache.shiro.web.servlet.SimpleCookie;
import org.apache.shiro.web.session.mgt.DefaultWebSessionManager;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.servlet.Filter;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
@Configuration
@Slf4j
@Data
public class ShiroConfig {
@Value("${shiro.session.timeout:1800000}")
private Long sessionTimeout;
@Value("${shiro.retry}")
private Integer retryLimit;
@Value("${shiro.lock}")
private Integer lockLimit;
@Value("${shiro.disabled:false}")
private boolean isDisabled;
@Value("${shiro.lock-duration}")
private Long lockDuration;
@Value("${spring.application.name}")
private String name;
@Value("${server.servlet.session.cookie.http-only:true}")
private Boolean httpOnly;
@Value("${server.servlet.session.cookie.secure:false}")
private Boolean secure;
@Value("${shiro.loginurl:/platform-user-service/login}")
private String loginUrl;
@Value("${shiro.overwrite.loginurl:}")
private String overWriteLoginUrl;
@Value("${shiro.jwt.urlPattern:/*}")
private String urlPattern;
@Value("${shiro.jwt.maxAliveMinute:30}")
private int maxAliveMinute;
@Value("${shiro.jwt.maxIdleMinute:60}")
private int maxIdleMinute;
@Value("${shiro.jwt.headerKeyOfToken:access_token}")
private String headerKeyOfToken;
@Value("${shiro.jwt.accountAlias:account}")
private String accountAlias;
@Value("${shiro.jwt.enableAutoRefreshToken:false}")
private boolean enableAutoRefreshToken;
@Autowired
private JWTUserAuthService userAuthService;
@Bean
public static LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
return new LifecycleBeanPostProcessor();
}
@Bean(name = "shiroFilter")
public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
shiroFilterFactoryBean.setSecurityManager(securityManager);
log.info("overwrite login url {}", overWriteLoginUrl);
if(overWriteLoginUrl == null || overWriteLoginUrl.isEmpty()){
shiroFilterFactoryBean.setLoginUrl(loginUrl);
}else{
shiroFilterFactoryBean.setLoginUrl(overWriteLoginUrl);
}
Map<String, Filter> filters = new HashMap();
filters.put(GlobalConstant.JWT_AUTHC, jwtAuthcFilter());
shiroFilterFactoryBean.setFilters(filters);
shiroFilterFactoryBean.setUnauthorizedUrl("/unauthorized");
Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
filterChainDefinitionMap.put("/css/**", "anon");
filterChainDefinitionMap.put("/img/**", "anon");
filterChainDefinitionMap.put("/images/**", "anon");
filterChainDefinitionMap.put("/js/**", "anon");
filterChainDefinitionMap.put("/plugins/**", "anon");
filterChainDefinitionMap.put("/login", "anon");
filterChainDefinitionMap.put("/token", "anon");
filterChainDefinitionMap.put("/api/v1.0/login", "anon");
filterChainDefinitionMap.put("/api/v1.0/token", "anon");
filterChainDefinitionMap.put("/api/v1.0/ping", "anon");
filterChainDefinitionMap.put("/api/v1.0/message", "anon");
filterChainDefinitionMap.put("/api/v1.0/user", GlobalConstant.JWT_AUTHC);
filterChainDefinitionMap.put("/**", "authc");
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
return shiroFilterFactoryBean;
}
@Bean
public SecurityManager securityManager() {
DefaultWebSecurityManager defaultWebSecurityManager = new DefaultWebSecurityManager();
defaultWebSecurityManager.setAuthenticator(modularRealmAuthenticator());
List<Realm> realms = new ArrayList<>();
realms.add(jwtShiroRealm());
realms.add(shiroRealm());
defaultWebSecurityManager.setRealms(realms);
defaultWebSecurityManager.setSessionManager(getDefaultWebSessionManager());
//defaultWebSecurityManager.setRememberMeManager(cookieRememberMeManager());
defaultWebSecurityManager.setCacheManager(ehCacheManager());
return defaultWebSecurityManager;
}
private DefaultWebSessionManager getDefaultWebSessionManager() {
DefaultWebSessionManager defaultWebSessionManager = new DefaultWebSessionManager();
defaultWebSessionManager.setGlobalSessionTimeout(sessionTimeout);
defaultWebSessionManager.setSessionIdCookie(getSessionIdCookie());
defaultWebSessionManager.setSessionIdCookieEnabled(true);
defaultWebSessionManager.setCacheManager(ehCacheManager());
defaultWebSessionManager.setSessionDAO(sessionDAO());
return defaultWebSessionManager;
}
@Bean
public EhCacheManager ehCacheManager() {
EhCacheManager ehCacheManager = new EhCacheManager();
ehCacheManager.setCacheManagerConfigFile("classpath:ehcache.xml");
return ehCacheManager;
}
private SimpleCookie rememberMeCookie() {
SimpleCookie simpleCookie = new SimpleCookie("rememberMe");
simpleCookie.setHttpOnly(true);
simpleCookie.setMaxAge(2592000);
return simpleCookie;
}
private SimpleCookie getSessionIdCookie() {
SimpleCookie simpleCookie = new SimpleCookie(name);
simpleCookie.setHttpOnly(httpOnly);
simpleCookie.setMaxAge(1000 * 60);
simpleCookie.setPath(StrUtil.SLASH);
simpleCookie.setSameSite(Cookie.SameSiteOptions.LAX);
simpleCookie.setSecure(secure);
return simpleCookie;
}
/**
* Remember my manager
*
* @author FastKing
* @date 12:52 2018/9/28
**/
private CookieRememberMeManager cookieRememberMeManager() {
CookieRememberMeManager cookieRememberMeManager = new CookieRememberMeManager();
cookieRememberMeManager.setCookie(rememberMeCookie());
cookieRememberMeManager.setCipherKey(Base64.decode("4AvVhmFLUs0KTA3Kprsdag=="));
return cookieRememberMeManager;
}
@Bean
public SessionDAO sessionDAO() {
EnterpriseCacheSessionDAO cacheSessionDAO = new EnterpriseCacheSessionDAO();
cacheSessionDAO.setActiveSessionsCacheName("shiro-activeSessionCache");
return cacheSessionDAO;
}
@Bean
public CredentialsMatcher retryLimitCredentialsMatcher() {
return new RetryLimitCredentialsMatcher(retryLimit, lockLimit, lockDuration);
}
@Bean
public JWTAuthcFilter jwtAuthcFilter() {
return new JWTAuthcFilter(GlobalConstant.HEADER_KEY_TOKEN, userAuthService, isDisabled);
}
@Bean
public ModularRealmAuthenticator modularRealmAuthenticator(){
ModularRealmAuthenticator modularRealmAuthenticator=new ModularRealmAuthenticator();
modularRealmAuthenticator.setAuthenticationStrategy(new FirstSuccessfulStrategy());
return modularRealmAuthenticator;
}
@Bean
public JWTShiroRealm jwtShiroRealm() {
JWTShiroRealm tokenRealm = new JWTShiroRealm(userAuthService, accountAlias, maxIdleMinute);
tokenRealm.setCachingEnabled(false);
return tokenRealm;
}
@Bean
public ShiroRealm shiroRealm() {
ShiroRealm shiroRealm = new ShiroRealm();
shiroRealm.setCredentialsMatcher(retryLimitCredentialsMatcher());
return shiroRealm;
}
}
2.3.5 JWTShiroRealm
import com.auth0.jwt.interfaces.DecodedJWT;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
@AllArgsConstructor
@Slf4j
public class JWTShiroRealm extends AuthorizingRealm {
private final JWTUserAuthService userAuthService;
private final String accountAlias;
private final int maxIdleMinute;
@Override
public boolean supports(AuthenticationToken token) {
return token instanceof JWTToken;
}
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
JWTPrincipal principal = (JWTPrincipal) principals.getPrimaryPrincipal();
SimpleAuthorizationInfo authInfo = new SimpleAuthorizationInfo();
UserInfo up = userAuthService.getUserInfo(principal.getAccount());
if (up != null && up.getPermissions() != null) {
authInfo.addStringPermissions(up.getPermissions());
}
return authInfo;
}
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken auth){
String token = (String) auth.getCredentials();
String username = JWTHelper.getAccount(token, accountAlias);
if (username == null) {
throw new AuthenticationException("無(wú)效的請(qǐng)求");
}
UserInfo user = userAuthService.getUserInfo(username);
if (user == null) {
throw new AuthenticationException("未找到用戶信息");
}
DecodedJWT jwt = JWTHelper.verify(token, user.getSecret(), maxIdleMinute);
if (jwt == null) {
throw new AuthenticationException("token已經(jīng)過(guò)期蝗敢,請(qǐng)重新登錄");
}
JWTPrincipal principal = new JWTPrincipal();
principal.setAccount(user.getAccount());
principal.setUserId(user.getUserId());
principal.setExpiresAt(jwt.getExpiresAt().getTime());
//這里實(shí)際上會(huì)將AuthenticationToken.getCredentials()與傳入的第二個(gè)參數(shù)credentials進(jìn)行比較
//第一個(gè)參數(shù)是登錄成功后,可以通過(guò)subject.getPrincipal獲取
return new SimpleAuthenticationInfo(principal, token, this.getName());
}
}
2.3.6 ShiroRealm
import cn.hutool.core.text.CharSequenceUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
import org.apache.shiro.authc.UsernamePasswordToken;
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 javax.annotation.Resource;
import java.util.Objects;
import java.util.Set;
@Slf4j
public class ShiroRealm extends AuthorizingRealm {
@Resource
private LoginService loginService;
@Resource
private RoleService roleService;
@Override
public boolean supports(AuthenticationToken token) {
return token instanceof UsernamePasswordToken;
}
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
UserInfoVO emsUserInfo = (UserInfoVO) principals.getPrimaryPrincipal();
Set<String> perms = roleService.selectPermsByRole(emsUserInfo.getRoleId());
Set<String> roles = roleService.selectRoleCodeByRole(emsUserInfo.getRoleId());
SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
authorizationInfo.setStringPermissions(perms);
authorizationInfo.setRoles(roles);
return authorizationInfo;
}
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) {
String loginId = (String) authenticationToken.getPrincipal();
UserInfoVO emsUserInfo = loginService.getEmsUserInfo(loginId);
if (Objects.isNull(emsUserInfo)) {
emsUserInfo = new UserInfoVO();
emsUserInfo.setPassword(CharSequenceUtil.EMPTY);
}
return new SimpleAuthenticationInfo(emsUserInfo, emsUserInfo.getPassword(), this.getName());
}
@Override
public boolean isPermitted(PrincipalCollection principals, String permission) {
UserInfoVO emsUserInfo = (UserInfoVO) principals.getPrimaryPrincipal();
if (Objects.isNull(emsUserInfo.getRoleMenuInfo())) {
return false;
}
return emsUserInfo.getRoleMenuInfo().getIsAdmin() || super.isPermitted(principals, permission);
}
@Override
public boolean hasRole(PrincipalCollection principals, String roleIdentifier) {
UserInfoVO emsUserInfo = (UserInfoVO) principals.getPrimaryPrincipal();
if (Objects.isNull(emsUserInfo.getRoleMenuInfo())) {
return false;
}
return emsUserInfo.getRoleMenuInfo().getIsAdmin() || super.isPermitted(principals, roleIdentifier);
}
}
2.4. 密碼加密
為了兼容web端和移動(dòng)端對(duì)密碼的統(tǒng)一足删,在web端使用的是通過(guò)JavaScript和Web Crypto API來(lái)實(shí)現(xiàn)對(duì)數(shù)據(jù)進(jìn)行端到端加密寿谴,因此移動(dòng)端同樣需要實(shí)現(xiàn)此加密算法。為了方便移動(dòng)端的開(kāi)發(fā)失受,使用Java封裝了這套加密庫(kù)讶泰,移動(dòng)端可以直接調(diào)用咏瑟。
2.5. JWT Token刷新
accessToken 的有效期由兩個(gè)配置構(gòu)成,maxAliveMinute 和 maxIdleMinute痪署,配置見(jiàn)下面的配置章節(jié)码泞。maxAliveMinute 定義了 accessToken 的理論過(guò)期時(shí)間,而 maxIdleMinute 定義了 accessToken 的最大生存周期狼犯。 在用戶管理模塊中增加了 HandlerInterceptor 用來(lái)處理 Token 的自動(dòng)刷新問(wèn)題余寥,如果傳入的 Token 已經(jīng)超過(guò) maxAliveMinute 設(shè)定的時(shí)間,但還沒(méi)有達(dá)到 maxIdleMinute 的限制悯森,則會(huì)自動(dòng)刷新該用戶的 accessToken 并添加在 response header宋舷,客戶端如果在響應(yīng)頭中發(fā)現(xiàn)有新的 token 返回,說(shuō)明當(dāng)前 token 即將失效,需要及時(shí)更新自身存儲(chǔ)的 token瓢姻。這個(gè)機(jī)制實(shí)際是提供一個(gè)窗口期祝蝠,讓客戶端安全的刷新 accessToken。
2.6. 系統(tǒng)配置
配置主要分為以下幾個(gè)部分:
2.6.1. Shiro session配置
shiro:
retry: 5 # 重試次數(shù) lock: 5 # 鎖定次數(shù) lock-duration: 1 # 鎖定時(shí)長(zhǎng) min disabled: false
session:
timeout: 1800000
loginurl: /login
2.6.2. Shiro JWT配置
shiro:
retry: 5 # 重試次數(shù)
lock: 5 # 鎖定次數(shù)
lock-duration: 1 # 鎖定時(shí)長(zhǎng) min
disabled: false # A&A開(kāi)關(guān)
session:
timeout: 1800000
loginurl: /login
jwt:
maxAliveMinute: 1 # jwt token過(guò)期時(shí)間,單位minutes
maxIdleMinute: 120 # Jwt token最大存活時(shí)間汹来,單位minutes
headerKeyOfToken: access_token # Jwt token的header key name
accountAlias: account # Jwt token account key name
enableAutoRefreshToken: true # 是否自動(dòng)刷新access token
urlPattern: /api/v1.0/* # 需要刷新token的API Pattern
注意urlPattern续膳,為了支持刷新token,定義了urlpattern收班,因此需要所有的服務(wù)都已a(bǔ)pi/v1.0作為前綴
2.7. 調(diào)用方式
2.7.1. web頁(yè)面基于session訪問(wèn)
在web前端頁(yè)面訪問(wèn)任一個(gè)API,都會(huì)跳轉(zhuǎn)到登錄頁(yè)面坟岔,輸入用戶名和密碼即可登錄。
2.7.2. Mobile基于JWT Token訪問(wèn)
login
curl -X POST [http://localhost:50000/api/v1.0/token](http://localhost:50000/api/v1.0/token) -H "accept: application/json" -H "Content-Type: application/json" -d "{\"loginId\":\"admin\",\"password\":\"8SLGGbu7IYXVx4DJ.IGcMdlUQkaxDHG82fbCNCMC7LzWgex40qAFMnQ==\"}"
在access_token中返回jwt token如下:
login response
{
"code": 200,
"message": "操作成功",
"data": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE2Mjc5Njg5MDUsImFjY291bnQiOiJhZG1pbiJ9.I5ToKyLKb22lxpo_LmA2mEHPXLMUUdmXm556LqRsHd0"
}
request api
curl -X POST [http://localhost:50000/api/v1.0/user](http://localhost:50000/api/v1.0/user) -H "accept: application/json" -H "Content-Type: application/json" -H "access_token:eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE2Mjc5Njg5MDUsImFjY291bnQiOiJhZG1pbiJ9.I5ToKyLKb22lxpo_LmA2mEHPXLMUUdmXm556LqRsHd0" -d "{\"userId\":8}"
寫(xiě)在最后
由于涉及到公司的一些業(yè)務(wù)代碼摔桦,因此不方便保留在代碼中社付,因此,上述代碼不能編譯成功邻耕,主要是如何實(shí)現(xiàn)多認(rèn)證系統(tǒng)的一個(gè)思路鸥咖,具體我也是參考下面的兩篇文章來(lái)實(shí)現(xiàn)。