去年我开发了一个适用于先后端分离项目的权限控制框架,后面我通过深思熟虑后决定开源出来,供你们使用以及参考其实现思路,就当作回馈社区吧。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;好了,限于篇幅,本篇文章到此就结束了,更多用法以及功能扩展方式,能够继续查看下面这几个连接: