去年我開發了一個適用於先後端分離項目的權限控制框架,後面我通過深思熟慮後決定開源出來,供你們使用以及參考其實現思路,就當作回饋社區吧。css
接下來我將分別從開發背景、開發思路、功能特性、簡單示例、核心流程幾個方面分別敘述。html
前幾年,那個時候的大部分系統都採用的是「服務端業務處理 + 前端頁面模板」的項目開發模式,所以這種系統的權限控制比較好處理,能夠經過後端校驗權限,並在權限校驗沒有經過的狀況下控制前端頁面跳轉到登陸頁面或者錯誤頁面。前端
目前這種項目開發模式的權限控制已經有比較成熟的方案——cookie-session認證機制,在Java Web
項目中其主流解決方案是使用Spring Security
或者Shiro
等框架完成系統的權限控制。java
可是,最近這幾年先後端分離項目開發模式開始逐漸興起,其目的是爲了解決前端和服務端耦合性太強的問題,方便前端頁面能夠隨時更改,獨立運行。此外,APP、微信公衆號、小程序也能夠看作是先後端分離項目模式的「前端」,由於核心業務邏輯仍然是在服務端處理,APP、微信公衆號、小程序則主要進行數據展現。git
在先後端分離項目開發模式中,後端再也不擁有對前端頁面的控制權,而這偏偏引發瞭如下幾個新的問題。github
其一,當後端判斷用戶沒有登陸或者權限不夠時沒法控制前端頁面跳轉到登陸頁面/未受權頁面;web
其二,在MVC
項目開發模式中,每當前端頁面請求後端都會攜帶當前用戶的session_id
,以便後端能夠在SessionDAO
中刷新用戶會話,保持在線狀態,可是在先後端分離項目開發模式中,前端頁面(注:這裏泛指瀏覽器、APP等數據展現端)可能很長時間纔會請求一次後端接口(注:好比用戶每隔幾小時甚至幾天纔打開一次APP),這種現象致使後端沒法讓用戶會話一直處於活動狀態,並且即便花費大量內存代價人爲延長用戶會話的時效時間,其作法也顯得極其低效;redis
其三,以往MVC
項目開發模式中成熟的cookie-session認證機制
嚴重依賴於瀏覽器的cookie
機制,在APP等沒有cookie的環境中將徹底無法使用。算法
由於上述所說的這幾個問題,因此致使在先後端分離項目開發模式中只能拋棄傳統的cookie-session認證機制
,從新尋找新的權限控制方式。目前在Java Web
項目中使用特別普遍的解決方案主要是:JSON Web Token (JWT)
。spring
所謂JWT
,本質上是一種特殊格式的字符串(token
),而後主要經過如下兩個步驟實現權限控制:
JSON Web Token (JWT)
。JWT一般由三部分組成: 頭信息(header),消息體(payload)和簽名(signature) 。頭信息指定了該JWT使用的簽名算法;消息體包含了JWT的意圖,好比令牌的過時時間,用戶主體信息等內容;最後簽名則主要是爲了確保消息數據不被篡改。cookie
、LocalStorage
等其餘客戶端存儲方案中,此後客戶端將在與服務端交互中都會帶上JWT。而後服務端在接收到JWT
後再驗證其是否合法,以及JWT中的權限信息是否被容許訪問當前資源。從JWT的實現原理咱們能夠看出,JWT解決了一部分先後端分離項目開發模式引起的問題,可是它並無徹底解決,並且JWT在管理用戶權限方面至少還存在如下幾個方面的缺點:
所以,在現現在WEB開發逐漸傾向於先後端分離、分佈式等現實背景下,一種實現方式簡單、功能完整、運行效率高且能夠避免JWT的諸多缺點的權限控制模型及現成可用的框架已經成爲一個亟待解決的問題。
在借鑑了JWT
和Apache Shiro
的實現思路後,我開發瞭如今正在給你們介紹的這個 easylimit 框架。
在實現上,首先我擴展了RBAC權限模型,引入了訪問令牌的概念。在 訪問令牌-RBAC 權限模型中,針對目前主流的RBAC權限模型進行了擴展,前端頁面(網頁、APP、微信公衆號、小程序等)再也不存儲用於區分服務端用戶會話的session_id,而是自行選擇如何存儲訪問令牌(access_token
)和刷新令牌(refresh_token
)。其中,refresh_token用於在access_token過時後請求服務端生成新的access_token,而access_token則跟服務端的用戶會話(session)一一對應,即一個access_token只對應於一個服務端的惟一用戶標識。所以在用戶攜帶access_token請求服務端後,服務端就能夠根據access_token查找到與之關聯的session,後面就跟RBAC權限模型的鑑權步驟同樣了,也就是:根據session中的用戶基本信息獲取用戶當前擁有的角色和權限,判斷當前用戶是否有權限請求該資源,若是沒有就返回錯誤提示,有則繼續往下執行。
具體來說,在前端頁面訪問後端服務的過程當中,後端服務主要會執行如下幾個核心操做:
與Shiro
相比,easylimit
這個框架的關鍵點在於,再也不使用「將session_id存儲到Cookie以便關聯用戶會話」的模式,而是經過給用戶返回訪問令牌(access_token)和刷新令牌(refresh_token),讓用戶靈活選擇如何存儲這兩個令牌,只要保證調用業務接口時攜帶上訪問令牌(access_token)便可。此外,經過將refresh_token和access_token關聯,保障了能夠經過refresh_token不斷生成新的access_token,經過access_token和存儲在服務端的session_id關聯,保障了能夠經過access_token找到請求用戶的會話(session),以便進行後續其餘鑑權操做,而這種作法也剛好避免了JWT
不能靈活做廢已頒發的令牌以及沒法隨時更新用戶權限信息的缺陷。
在使用上,easylimit
須要依賴spring-context
、Jackson
、Jedis
這幾個組件,而後主要提供瞭如下功能特性:
MVC
和先後端分離
項目開發模式的權限控制RBAC
權限控制session_id
生成方式,包括:隨機字符串
、UUID
、雪花算法
session
和token
存儲方式,包括:基於ConcurrentHashMap
的內存存儲、使用Redis
等緩存存儲@RequiresLogin
、@RequiresPermissions
、@RequiresRoles
Access Token
傳參方式,且能夠靈活擴展Base64
、Md5Hex
、Sha256Hex
、Sha512Hex
、Md5Crypt
、Sha256Crypt
等其餘自定義密碼加密/摘要方式pom.xml
中添加依賴:<dependency>
<groupId>cn.zifangsky</groupId>
<artifactId>easylimit</artifactId>
<version>1.0.0-RELEASE</version>
</dependency>
複製代碼
package cn.zifangsky.easylimit.example.easylimit;
import cn.zifangsky.easylimit.access.Access;
import cn.zifangsky.easylimit.authc.PrincipalInfo;
import cn.zifangsky.easylimit.authc.ValidatedInfo;
import cn.zifangsky.easylimit.authc.impl.SimplePrincipalInfo;
import cn.zifangsky.easylimit.authc.impl.UsernamePasswordValidatedInfo;
import cn.zifangsky.easylimit.example.mapper.SysFunctionMapper;
import cn.zifangsky.easylimit.example.mapper.SysRoleMapper;
import cn.zifangsky.easylimit.example.mapper.SysUserMapper;
import cn.zifangsky.easylimit.example.model.SysFunction;
import cn.zifangsky.easylimit.example.model.SysRole;
import cn.zifangsky.easylimit.example.model.SysUser;
import cn.zifangsky.easylimit.exception.authc.AuthenticationException;
import cn.zifangsky.easylimit.permission.PermissionInfo;
import cn.zifangsky.easylimit.permission.impl.SimplePermissionInfo;
import cn.zifangsky.easylimit.realm.impl.AbstractPermissionRealm;
import cn.zifangsky.easylimit.utils.SecurityUtils;
import java.util.HashSet;
import java.util.Set;
import java.util.stream.Collectors;
/** * 自定義{@link cn.zifangsky.easylimit.realm.Realm} * * @author zifangsky * @date 2019/5/28 * @since 1.0.0 */
public class CustomRealm extends AbstractPermissionRealm {
private SysUserMapper sysUserMapper;
private SysRoleMapper sysRoleMapper;
private SysFunctionMapper sysFunctionMapper;
public CustomRealm(SysUserMapper sysUserMapper, SysRoleMapper sysRoleMapper, SysFunctionMapper sysFunctionMapper) {
this.sysUserMapper = sysUserMapper;
this.sysRoleMapper = sysRoleMapper;
this.sysFunctionMapper = sysFunctionMapper;
}
/** * 自定義「角色+權限」信息的獲取方式 */
@Override
protected PermissionInfo doGetPermissionInfo(PrincipalInfo principalInfo) {
SimplePermissionInfo permissionInfo = null;
//獲取用戶信息
SysUser sysUser = (SysUser) principalInfo.getPrincipal();
if(sysUser != null){
//經過用戶ID查詢角色權限信息
Set<SysRole> roleSet = sysRoleMapper.selectByUserId(sysUser.getId());
if(roleSet != null && roleSet.size() > 0){
//全部角色名
Set<String> roleNames = new HashSet<>(roleSet.size());
//全部權限的code集合
Set<String> funcCodes = new HashSet<>();
for(SysRole role : roleSet){
roleNames.add(role.getName());
Set<SysFunction> functionSet = sysFunctionMapper.selectByRoleId(role.getId());
if(functionSet != null && functionSet.size() > 0){
funcCodes.addAll(functionSet.stream().map(SysFunction::getPathUrl).collect(Collectors.toSet()));
}
}
//實例化
permissionInfo = new SimplePermissionInfo(roleNames, funcCodes);
}
}
return permissionInfo;
}
/** * 自定義從表單的驗證信息獲取數據庫中正確的用戶主體信息 */
@Override
protected PrincipalInfo doGetPrincipalInfo(ValidatedInfo validatedInfo) throws AuthenticationException {
//已知是「用戶名+密碼」的登陸模式
UsernamePasswordValidatedInfo usernamePasswordValidatedInfo = (UsernamePasswordValidatedInfo) validatedInfo;
SysUser sysUser = sysUserMapper.selectByUsername(usernamePasswordValidatedInfo.getSubject());
return new SimplePrincipalInfo(sysUser, sysUser.getUsername(), sysUser.getPassword());
}
/** * <p>提示:在修改用戶主體信息、角色、權限等接口時,須要手動調用此方法清空緩存的PrincipalInfo和PermissionInfo</p> */
protected void clearCache() {
//1. 獲取本次請求實例
Access access = SecurityUtils.getAccess();
//2. 獲取PrincipalInfo
PrincipalInfo principalInfo = access.getPrincipalInfo();
//3. 清理緩存
super.doClearCache(principalInfo);
}
}
複製代碼
easylimit
框架的配置:package cn.zifangsky.easylimit.example.config;
import cn.zifangsky.easylimit.DefaultWebSecurityManager;
import cn.zifangsky.easylimit.SecurityManager;
import cn.zifangsky.easylimit.cache.Cache;
import cn.zifangsky.easylimit.cache.impl.DefaultRedisCache;
import cn.zifangsky.easylimit.enums.ProjectModeEnums;
import cn.zifangsky.easylimit.example.easylimit.CustomRealm;
import cn.zifangsky.easylimit.example.mapper.SysFunctionMapper;
import cn.zifangsky.easylimit.example.mapper.SysRoleMapper;
import cn.zifangsky.easylimit.example.mapper.SysUserMapper;
import cn.zifangsky.easylimit.filter.impl.support.DefaultFilterEnums;
import cn.zifangsky.easylimit.filter.impl.support.FilterRegistrationFactoryBean;
import cn.zifangsky.easylimit.permission.aop.PermissionsAnnotationAdvisor;
import cn.zifangsky.easylimit.realm.Realm;
import cn.zifangsky.easylimit.session.SessionDAO;
import cn.zifangsky.easylimit.session.SessionIdFactory;
import cn.zifangsky.easylimit.session.SessionManager;
import cn.zifangsky.easylimit.session.impl.AbstractWebSessionManager;
import cn.zifangsky.easylimit.session.impl.MemorySessionDAO;
import cn.zifangsky.easylimit.session.impl.support.CookieWebSessionManager;
import cn.zifangsky.easylimit.session.impl.support.RandomCharacterSessionIdFactory;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.web.filter.DelegatingFilterProxy;
import java.time.temporal.ChronoUnit;
import java.util.LinkedHashMap;
import java.util.concurrent.TimeUnit;
/** * EasyLimit框架的配置 * * @author zifangsky * @date 2019/5/28 * @since 1.0.0 */
@Configuration
public class EasyLimitConfig {
/** * 配置緩存 */
@Bean
public Cache cache(RedisTemplate<String, Object> redisTemplate){
return new DefaultRedisCache(redisTemplate);
}
/** * 配置Realm */
@Bean
public Realm realm(SysUserMapper sysUserMapper, SysRoleMapper sysRoleMapper, SysFunctionMapper sysFunctionMapper, Cache cache){
CustomRealm realm = new CustomRealm(sysUserMapper, sysRoleMapper, sysFunctionMapper);
//緩存主體信息
realm.setEnablePrincipalInfoCache(true);
realm.setPrincipalInfoCache(cache);
//緩存角色、權限信息
realm.setEnablePermissionInfoCache(true);
realm.setPermissionInfoCache(cache);
return realm;
}
/** * 配置Session的存儲方式 */
@Bean
public SessionDAO sessionDAO(Cache cache){
return new MemorySessionDAO();
}
/** * 配置session管理器 */
@Bean
public AbstractWebSessionManager sessionManager(SessionDAO sessionDAO){
// CookieInfo cookieInfo = new CookieInfo("custom_session_id");
AbstractWebSessionManager sessionManager = new CookieWebSessionManager(/*cookieInfo*/);
sessionManager.setSessionDAO(sessionDAO);
//設置session超時時間爲1小時
sessionManager.setGlobalTimeout(1L);
sessionManager.setGlobalTimeoutChronoUnit(ChronoUnit.HOURS);
//設置定時校驗的時間爲2分鐘
sessionManager.setSessionValidationInterval(2L);
sessionManager.setSessionValidationUnit(TimeUnit.MINUTES);
//設置sessionId的生成方式
// SessionIdFactory sessionIdFactory = new SnowFlakeSessionIdFactory(1L, 1L);
SessionIdFactory sessionIdFactory = new RandomCharacterSessionIdFactory();
sessionManager.setSessionIdFactory(sessionIdFactory);
return sessionManager;
}
/** * 認證、權限、session等管理的入口 */
@Bean
public SecurityManager securityManager(Realm realm, SessionManager sessionManager){
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(realm, sessionManager);
//踢出當前用戶的舊會話
securityManager.setKickOutOldSessions(true);
return securityManager;
}
/** * 將filter添加到Spring管理 */
@Bean
public FilterRegistrationFactoryBean filterRegistrationFactoryBean(SecurityManager securityManager){
//添加指定路徑的權限校驗
LinkedHashMap<String, String[]> patternPathFilterMap = new LinkedHashMap<>();
patternPathFilterMap.put("/css/**", new String[]{DefaultFilterEnums.ANONYMOUS.getFilterName()});
patternPathFilterMap.put("/layui/**", new String[]{DefaultFilterEnums.ANONYMOUS.getFilterName()});
patternPathFilterMap.put("/index.html", new String[]{DefaultFilterEnums.ANONYMOUS.getFilterName()});
patternPathFilterMap.put("/test/greeting", new String[]{DefaultFilterEnums.ANONYMOUS.getFilterName()});
// patternPathFilterMap.put("/test/selectByUsername", new String[]{"perms[/aaa/bbb]"});
//其餘路徑須要登陸才能訪問
patternPathFilterMap.put("/**/*.html", new String[]{DefaultFilterEnums.LOGIN.getFilterName()});
FilterRegistrationFactoryBean factoryBean = new FilterRegistrationFactoryBean(ProjectModeEnums.DEFAULT, securityManager, patternPathFilterMap);
//設置幾個登陸、未受權等相關URL
factoryBean.setLoginUrl("/login.html");
factoryBean.setLoginCheckUrl("/check");
factoryBean.setUnauthorizedUrl("/error.html");
return factoryBean;
}
@Bean
public FilterRegistrationBean<DelegatingFilterProxy> delegatingFilterProxy() {
FilterRegistrationBean<DelegatingFilterProxy> filterRegistrationBean = new FilterRegistrationBean<>();
DelegatingFilterProxy proxy = new DelegatingFilterProxy();
proxy.setTargetFilterLifecycle(true);
proxy.setTargetBeanName("filterRegistrationFactoryBean");
filterRegistrationBean.setFilter(proxy);
return filterRegistrationBean;
}
/** * 添加對權限註解的支持 */
@Bean
public PermissionsAnnotationAdvisor permissionsAnnotationAdvisor(){
return new PermissionsAnnotationAdvisor("execution(* cn.zifangsky..controller..*.*(..))");
}
}
複製代碼
登陸註銷相關示例:
/** * 登陸驗證 * @author zifangsky * @date 2019/5/29 13:23 * @since 1.0.0 * @return java.util.Map<java.lang.String,java.lang.Object> */
@PostMapping(value = "/check", produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
@ResponseBody
public Map<String,Object> check(HttpServletRequest request){
Map<String,Object> result = new HashMap<>(4);
result.put("code",500);
try {
//用戶名
String username = request.getParameter("username");
//密碼
String password = request.getParameter("password");
//獲取本次請求實例
Access access = SecurityUtils.getAccess();
if(StringUtils.isBlank(username) || StringUtils.isBlank(password)){
result.put("msg","請求參數不能爲空!");
return result;
}else{
logger.debug(MessageFormat.format("用戶[{0}]正在請求登陸", username));
//設置驗證信息
ValidatedInfo validatedInfo = new UsernamePasswordValidatedInfo(username, password, EncryptionTypeEnums.Sha256Crypt);
//1. 登陸驗證
access.login(validatedInfo);
}
Session session = access.getSession();
//2. 返回給頁面的數據
//登陸成功以後的回調地址
String redirectUrl = (String) session.getAttribute(cn.zifangsky.easylimit.common.Constants.SAVED_SOURCE_URL_NAME);
session.removeAttribute(cn.zifangsky.easylimit.common.Constants.SAVED_SOURCE_URL_NAME);
if(StringUtils.isNoneBlank(redirectUrl)){
result.put("redirect_uri", redirectUrl);
}
result.put("code",200);
}catch (Exception e){
result.put("code", 500);
result.put("msg", "登陸失敗,用戶名或密碼錯誤!");
logger.error("登陸失敗",e);
}
return result;
}
/** * 退出登陸 * @author zifangsky * @date 2019/5/29 17:44 * @since 1.0.0 * @return java.util.Map<java.lang.String,java.lang.Object> */
@PostMapping(value = "/logout.html", produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
@ResponseBody
public Map<String,Object> logout(HttpServletRequest request){
Map<String,Object> result = new HashMap<>(1);
Access access = SecurityUtils.getAccess();
SysUser user = (SysUser) access.getPrincipalInfo().getPrincipal();
if(user != null){
logger.debug(MessageFormat.format("用戶[{0}]正在退出登陸", user.getUsername()));
}
try {
//1. 退出登陸
access.logout();
//2. 返回狀態碼
result.put("code", 200);
}catch (Exception e){
result.put("code",500);
}
return result;
}
複製代碼
權限校驗註解示例:
目前默認提供瞭如下三個權限校驗註解(可擴展):
@ResponseBody
@RequiresPermissions("/aaa/bbb")
@RequestMapping(value = "/selectByUsername", produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
public SysUser selectByUsername(String username) {
return testService.selectByUsername(username);
}
複製代碼
pom.xml
中添加依賴:這一步不用多說,跟上面同樣。
這一步也跟上面同樣。
easylimit
框架的配置:在easylimit
框架的配置過程當中,與MVC
項目開發模式相比不一樣的地方有三處。第一是須要將SessionManager
設置爲TokenWebSessionManager
及其子類;第二是須要將SecurityManager
設置爲TokenWebSecurityManager
及其子類;第三是須要設置當前項目模式爲ProjectModeEnums.TOKEN
。
package cn.zifangsky.easylimit.token.example.config;
import cn.zifangsky.easylimit.SecurityManager;
import cn.zifangsky.easylimit.TokenWebSecurityManager;
import cn.zifangsky.easylimit.cache.Cache;
import cn.zifangsky.easylimit.cache.impl.DefaultRedisCache;
import cn.zifangsky.easylimit.enums.ProjectModeEnums;
import cn.zifangsky.easylimit.filter.impl.support.DefaultFilterEnums;
import cn.zifangsky.easylimit.filter.impl.support.FilterRegistrationFactoryBean;
import cn.zifangsky.easylimit.permission.aop.PermissionsAnnotationAdvisor;
import cn.zifangsky.easylimit.realm.Realm;
import cn.zifangsky.easylimit.session.SessionDAO;
import cn.zifangsky.easylimit.session.SessionIdFactory;
import cn.zifangsky.easylimit.session.TokenDAO;
import cn.zifangsky.easylimit.session.impl.DefaultTokenOperateResolver;
import cn.zifangsky.easylimit.session.impl.MemorySessionDAO;
import cn.zifangsky.easylimit.session.impl.support.DefaultCacheTokenDAO;
import cn.zifangsky.easylimit.session.impl.support.RandomCharacterSessionIdFactory;
import cn.zifangsky.easylimit.session.impl.support.TokenInfo;
import cn.zifangsky.easylimit.session.impl.support.TokenWebSessionManager;
import cn.zifangsky.easylimit.token.example.easylimit.CustomRealm;
import cn.zifangsky.easylimit.token.example.mapper.SysFunctionMapper;
import cn.zifangsky.easylimit.token.example.mapper.SysRoleMapper;
import cn.zifangsky.easylimit.token.example.mapper.SysUserMapper;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.web.filter.DelegatingFilterProxy;
import java.time.temporal.ChronoUnit;
import java.util.LinkedHashMap;
import java.util.concurrent.TimeUnit;
/** * EasyLimit框架的配置 * * @author zifangsky * @date 2019/5/28 * @since 1.0.0 */
@Configuration
public class EasyLimitConfig {
/** * 配置緩存 */
@Bean
public Cache cache(RedisTemplate<String, Object> redisTemplate){
return new DefaultRedisCache(redisTemplate);
}
/** * 配置Realm */
@Bean
public Realm realm(SysUserMapper sysUserMapper, SysRoleMapper sysRoleMapper, SysFunctionMapper sysFunctionMapper, Cache cache){
CustomRealm realm = new CustomRealm(sysUserMapper, sysRoleMapper, sysFunctionMapper);
//緩存主體信息
realm.setEnablePrincipalInfoCache(true);
realm.setPrincipalInfoCache(cache);
//緩存角色、權限信息
realm.setEnablePermissionInfoCache(true);
realm.setPermissionInfoCache(cache);
return realm;
}
/** * 配置Session的存儲方式 */
@Bean
public SessionDAO sessionDAO(Cache cache){
return new MemorySessionDAO();
}
/** * 配置Token的存儲方式 */
@Bean
public TokenDAO tokenDAO(Cache cache){
return new DefaultCacheTokenDAO(cache);
}
/** * 配置session管理器 */
@Bean
public TokenWebSessionManager sessionManager(SessionDAO sessionDAO, TokenDAO tokenDAO){
TokenInfo tokenInfo = new TokenInfo();
tokenInfo.setAccessTokenTimeout(2L);
tokenInfo.setAccessTokenTimeoutUnit(ChronoUnit.MINUTES);
tokenInfo.setRefreshTokenTimeout(1L);
tokenInfo.setRefreshTokenTimeoutUnit(ChronoUnit.DAYS);
//建立基於Token的session管理器
TokenWebSessionManager sessionManager = new TokenWebSessionManager(tokenInfo,new DefaultTokenOperateResolver(), tokenDAO);
sessionManager.setSessionDAO(sessionDAO);
//設置定時校驗的時間爲3分鐘
sessionManager.setSessionValidationInterval(3L);
sessionManager.setSessionValidationUnit(TimeUnit.MINUTES);
//設置sessionId的生成方式
SessionIdFactory sessionIdFactory = new RandomCharacterSessionIdFactory();
sessionManager.setSessionIdFactory(sessionIdFactory);
return sessionManager;
}
/** * 認證、權限、session等管理的入口 */
@Bean
public SecurityManager securityManager(Realm realm, TokenWebSessionManager sessionManager){
return new TokenWebSecurityManager(realm, sessionManager);
}
/** * 將filter添加到Spring管理 */
@Bean
public FilterRegistrationFactoryBean filterRegistrationFactoryBean(SecurityManager securityManager){
//添加指定路徑的權限校驗
LinkedHashMap<String, String[]> patternPathFilterMap = new LinkedHashMap<>();
patternPathFilterMap.put("/css/**", new String[]{DefaultFilterEnums.ANONYMOUS.getFilterName()});
patternPathFilterMap.put("/layui/**", new String[]{DefaultFilterEnums.ANONYMOUS.getFilterName()});
// patternPathFilterMap.put("/test/greeting", new String[]{DefaultFilterEnums.ANONYMOUS.getFilterName()});
patternPathFilterMap.put("/refreshToken", new String[]{DefaultFilterEnums.ANONYMOUS.getFilterName()});
// patternPathFilterMap.put("/test/selectByUsername", new String[]{"perms[/aaa/bbb]"});
//其餘路徑須要登陸才能訪問
patternPathFilterMap.put("/**", new String[]{DefaultFilterEnums.LOGIN.getFilterName()});
FilterRegistrationFactoryBean factoryBean = new FilterRegistrationFactoryBean(ProjectModeEnums.TOKEN, securityManager, patternPathFilterMap);
//設置幾個登陸、未受權等相關URL
factoryBean.setLoginCheckUrl("/login");
return factoryBean;
}
@Bean
public FilterRegistrationBean<DelegatingFilterProxy> delegatingFilterProxy() {
FilterRegistrationBean<DelegatingFilterProxy> filterRegistrationBean = new FilterRegistrationBean<>();
DelegatingFilterProxy proxy = new DelegatingFilterProxy();
proxy.setTargetFilterLifecycle(true);
proxy.setTargetBeanName("filterRegistrationFactoryBean");
filterRegistrationBean.setFilter(proxy);
return filterRegistrationBean;
}
/** * 添加對權限註解的支持 */
@Bean
public PermissionsAnnotationAdvisor permissionsAnnotationAdvisor(){
return new PermissionsAnnotationAdvisor("execution(* cn.zifangsky..controller..*.*(..))");
}
}
複製代碼
登陸註銷相關示例:
/** * 登陸驗證 * @author zifangsky * @date 2019/5/29 13:23 * @since 1.0.0 * @return java.util.Map<java.lang.String,java.lang.Object> */
@PostMapping(value = "/login", produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
@ResponseBody
public Map<String,Object> check(HttpServletRequest request){
Map<String,Object> result = new HashMap<>(4);
result.put("code",500);
try {
//用戶名
String username = request.getParameter("username");
//密碼
String password = request.getParameter("password");
//獲取本次請求實例
ExposedTokenAccess access = (ExposedTokenAccess) SecurityUtils.getAccess();
if(StringUtils.isBlank(username) || StringUtils.isBlank(password)){
result.put("msg","請求參數不能爲空!");
return result;
}else{
logger.debug(MessageFormat.format("用戶[{0}]正在請求登陸", username));
//設置驗證信息
ValidatedInfo validatedInfo = new UsernamePasswordValidatedInfo(username, password, EncryptionTypeEnums.Sha256Crypt);
//1. 登陸驗證
access.login(validatedInfo);
}
//2. 獲取Access Token和Refresh Token
SimpleAccessToken accessToken = access.getAccessToken();
SimpleRefreshToken refreshToken = access.getRefreshToken();
//3. 返回給頁面的數據
result.put("code",200);
result.put("access_token", accessToken.getAccessToken());
result.put("refresh_token", refreshToken.getRefreshToken());
result.put("expires_in", accessToken.getExpiresIn());
// result.put("user_info", accessToken.getPrincipalInfo().getPrincipal());
}catch (Exception e){
result.put("code", 500);
result.put("msg", "登陸失敗,用戶名或密碼錯誤!");
logger.error("登陸失敗",e);
}
return result;
}
/** * 退出登陸 * @author zifangsky * @date 2019/5/29 17:44 * @since 1.0.0 * @return java.util.Map<java.lang.String,java.lang.Object> */
@PostMapping(value = "/logout", produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
@ResponseBody
public Map<String,Object> logout(HttpServletRequest request){
Map<String,Object> result = new HashMap<>(1);
Access access = SecurityUtils.getAccess();
SysUser user = (SysUser) access.getPrincipalInfo().getPrincipal();
if(user != null){
logger.debug(MessageFormat.format("用戶[{0}]正在退出登陸", user.getUsername()));
}
try {
//1. 退出登陸
access.logout();
//2. 返回狀態碼
result.put("code", 200);
}catch (Exception e){
result.put("code",500);
}
return result;
}
複製代碼
刷新Access Token
相關示例:
/** * 刷新Access Token * @author zifangsky * @date 2019/5/29 13:23 * @since 1.0.0 * @return java.util.Map<java.lang.String,java.lang.Object> */
@RequestMapping(value = "/refreshToken", produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
@ResponseBody
public Map<String,Object> refreshAccessToken(HttpServletRequest request){
Map<String,Object> result = new HashMap<>(4);
result.put("code",500);
try {
//Refresh Token
String refreshTokenStr = request.getParameter("refresh_token");
//獲取本次請求實例
ExposedTokenAccess access = (ExposedTokenAccess) SecurityUtils.getAccess();
//1. 刷新Access Token
SimpleAccessToken newAccessToken = access.refreshAccessToken(refreshTokenStr);
//2. 返回給頁面的數據
result.put("code",200);
result.put("access_token", newAccessToken.getAccessToken());
result.put("expires_in", newAccessToken.getExpiresIn());
result.put("refresh_token", refreshTokenStr);
}catch (Exception e){
result.put("code", 500);
result.put("msg", "Refresh Token不可用!");
logger.error("Refresh Token不可用",e);
}
return result;
}
複製代碼
權限校驗註解示例:
基本用法跟MVC項目開發模式同樣,可是不一樣的地方有兩點。第一是請求接口的時候須要傳遞Access Token
,默認支持如下三種方式傳參(規則定義在cn/zifangsky/easylimit/session/impl/support/TokenWebSessionManager.java
的getAccessTokenFromRequest()
方法):
url
參數或者form-data
參數中攜帶了Access Token
(其名稱在上述配置的TokenInfo
類中定義)header
參數中攜帶了Access Token
(其名稱同上)header
參數中攜帶了Access Token
(其名稱爲Authorization
)第二是在沒有要求的角色/權限時,系統只會返回對應的狀態碼和提示信息,而不會像在MVC項目開發模式那樣直接重定向到登陸頁面。示例接口以及返回的錯誤提示以下所示:
@ResponseBody
@RequiresPermissions("/aaa/bbb")
@RequestMapping(value = "/selectByUsername", produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
public SysUser selectByUsername(String username) {
return testService.selectByUsername(username);
}
複製代碼
錯誤提示信息:
{
"name": "no_permissions",
"msg": "您當前沒有權限訪問該地址!",
"code": 403
}
複製代碼
接下來我再簡單介紹兩個框架運行過程當中的核心流程,包括:請求API接口執行流程以及經過refresh_toke
刷新access_token
流程。
SimpleAccessToken
對象中取出關聯的session_id;SessionDAO
獲取Session,此Session即爲當前用戶的會話信息,包含了當前訪問用戶擁有的角色、權限等基本信息。進一步地,在建立完session後,爲了方便在請求過程當中調用框架暴露出來的對外功能,所以還須要爲當前請求建立請求實例。所以,所述建立當前訪問實例(access)的步驟具體表現爲:
TokenAccessContext
對象,設置當前訪問實例的環境環境,好比:session、access_token、ServletRequest、ServletResponse等信息;TokenAccessFactory
類的工廠方法建立Access實例;至此,在建立完session和access後,filter
模塊將根據預先設置的規則校驗當前請求接口是否須要登陸才能訪問、是否須要擁有指定角色或者權限才能訪問。所以,filter模塊的鑑權邏輯步驟具體表現爲:
Access.login(...)
方法能夠進行登陸操做。refresh_toke
刷新access_token
流程:在這個流程中,除了統一執行的建立用戶會話 (Session)和建立當前請求實例(Access)這兩個步驟,還須要進行如下操做:
TokenDAO
獲取SimpleRefreshToken
對象;TokenOperateResolver
對象生成新的access_token和refresh_token,並從TokenDAO中移除舊的access_token;Access
;最後,若是判斷某個用戶帳號存在風險,管理人員也能夠在管理端系統強制該用戶下線。大致上須要執行如下幾個步驟:
expired
設置爲true;好了,限於篇幅,本篇文章到此就結束了,更多用法以及功能擴展方式,能夠繼續查看下面這幾個連接: