一.权限登录模块包括几个基本子模块:
1.登录。
实现方式大致为:先检验用户名密码是否正确,如正确则在缓存中存入用户信息(一般必须要有用户标识和访问token,或再加一些附加信息如用户的角色权限),再返回访问token给客户端。
2.过滤器,主要通过客户端访问时带的token检验是否有访问url权限。
实现方式大致为:通过客户端访问的url匹配资源类型。一些资源可直接放行,一些资源需要校验登录,一些资源需要校验角色权限。
3.获取角色权限的方式。
因为取权限角色的方式不同,一般权限框架会提供一层抽象(接口),需要开发者实现。取角色权限未必需要在过滤器中调用,可以在任何需要的时候调用。
根据缓存方式不同可以做一层抽象(接口)。
Sa-Token框架的核心类是cn.dev33.satoken.stp.StpLogic,该类实现了大部分简单逻辑操做功能。
二.登录。
1.Sa-token默认登录操作cn.dev33.satoken.stp.StpLogic#login(Object, SaLoginModel),第一个参数为自定义用户的凭证
cn.dev33.satoken.stp.StpLogic#login==>cn.dev33.satoken.stp.StpLogic#createLoginSession
/**
* 创建指定账号id的登录会话
* @param id 登录id,建议的类型:(long | int | String)
* @param loginModel 此次登录的参数Model
* @return 返回会话令牌
*/
public String createLoginSession(Object id, SaLoginModel loginModel) {
// ------ 前置检查
SaTokenException.throwByNull(id, "账号id不能为空");
// ------ 1、初始化 loginModel
SaTokenConfig config = getConfig();
loginModel.build(config);
// ------ 2、分配一个可用的 Token
String tokenValue = distUsableToken(id, loginModel);
// ------ 3. 获取 User-Session , 续期
SaSession session = getSessionByLoginId(id, true);
session.updateMinTimeout(loginModel.getTimeout());
// 在 User-Session 上记录token签名
session.addTokenSign(tokenValue, loginModel.getDeviceOrDefault());
// ------ 4. 持久化其它数据
// token -> id 映射关系
saveTokenToIdMapping(tokenValue, id, loginModel.getTimeout());
// 写入 [token-last-activity]
setLastActivityToNow(tokenValue);
// $$ 发布事件:账号xxx 登录成功
SaTokenEventCenter.doLogin(loginType, id, tokenValue, loginModel);
// 检查此账号会话数量是否超出最大值
if(config.getMaxLoginCount() != -1) {
logoutByMaxLoginCount(id, session, null, config.getMaxLoginCount());
}
// 返回Token
return tokenValue;
}
上面第2步生成token后,第3步底层在缓存中添加token和session对象的映射。
上面第4步底层又存入了token和用户凭证的映射关系
第三方框架snowy在登录时又在缓存中存了用户权限角色基本信息,方便单点登录时取权限角色信息(存在下面session的dataMap中)
底层先在缓存创建一个新的session在缓存中,在更新了一次数据。调用了2次缓存操作
三.过滤器。
过滤器配置源码
package com.wisdomcity.laian.river.config;
import cn.dev33.satoken.context.SaHolder;
import cn.dev33.satoken.context.model.SaResponse;
import cn.dev33.satoken.filter.SaServletFilter;
import cn.dev33.satoken.interceptor.SaAnnotationInterceptor;
import cn.dev33.satoken.router.SaHttpMethod;
import cn.dev33.satoken.router.SaRouter;
import cn.dev33.satoken.stp.StpInterface;
import cn.dev33.satoken.stp.StpLogic;
import cn.dev33.satoken.stp.StpUtil;
import cn.dev33.satoken.strategy.SaStrategy;
import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.util.CharsetUtil;
import cn.hutool.http.ContentType;
import cn.hutool.http.Header;
import cn.zy.genius.geomatics.basic.ResultCode;
import com.wisdomcity.laian.river.utils.GlobalExceptionUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import vip.xiaonuo.auth.core.enums.SaClientTypeEnum;
import vip.xiaonuo.auth.core.util.StpLoginUserUtil;
import vip.xiaonuo.common.pojo.CommonResult;
import java.util.List;
@Slf4j
@Configuration
public class SaTokenConfig implements WebMvcConfigurer {
/**
* 注册Sa-Token的注解拦截器,打开注解式鉴权功能
* <p>
* 注解的方式有以下几中,注解既可以加在接口方法上,也可加在Controller类上:
* 1.@SaCheckLogin: 登录认证 —— 只有登录之后才能进入该方法(常用)
* 2.@SaCheckRole("admin"): 角色认证 —— 必须具有指定角色标识才能进入该方法(常用)
* 3.@SaCheckPermission("user:add"): 权限认证 —— 必须具有指定权限才能进入该方法(常用)
* 4.@SaCheckSafe: 二级认证校验 —— 必须二级认证之后才能进入该方法
* 5.@SaCheckBasic: HttpBasic认证 —— 只有通过 Basic 认证后才能进入该方法
* <p>
* 在Controller中创建一个接口,默认不需要登录也不需要任何权限都可以访问的,只有加了上述注解才会校验
**/
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 注册注解拦截器,并排除不需要注解鉴权的接口地址 (与登录拦截器无关,只是说明哪些接口不需要被拦截器拦截,此处都拦截)
registry.addInterceptor(new SaAnnotationInterceptor()).addPathPatterns("/**");
}
@Bean("stpLogic")
public StpLogic getStpLogic() {
// 重写Sa-Token的StpLogic,默认客户端类型为B
return new StpLogic(SaClientTypeEnum.B.getValue());
}
@Bean("stpClientLogic")
public StpLogic getStpClientLogic() {
// 重写Sa-Token的StpLogic,默认客户端类型为C
return new StpLogic(SaClientTypeEnum.C.getValue());
}
@Bean
public void rewriteSaStrategy() {
// 重写Sa-Token的注解处理器,增加注解合并功能
SaStrategy.me.getAnnotation = AnnotatedElementUtils::getMergedAnnotation;
}
/**
* 权限认证接口实现类,集成权限认证功能
**/
@Component
public static class StpInterfaceImpl implements StpInterface {
/**
* 返回一个账号所拥有的权限码集合
*/
@Override
public List<String> getPermissionList(Object loginId, String loginType) {
return StpLoginUserUtil.getLoginUser().getPermissionCodeList();
}
/**
* 返回一个账号所拥有的角色标识集合
*/
@Override
public List<String> getRoleList(Object loginId, String loginType) {
return StpLoginUserUtil.getLoginUser().getRoleCodeList();
}
}
/**
* 无需登录的接口地址集合
*/
private static final String[] NO_LOGIN_PATH_ARR = {
/* 主入口 */
"/",
/* 静态资源 */
"/static/fonts/**",
"/static/icons/**",
"/favicon.ico",
"/doc.html",
"/webjars/**",
"/swagger-resources/**",
"/swagger-ui/**",
"/v2/api-docs",
"/v2/api-docs-ext",
"/v3/api-docs",
"/v3/api-docs-ext",
"/configuration/ui",
"/configuration/security",
"/ureport/**",
"/druid/**",
/* 文件预览 */
"/file/online/view",
};
/**
* 仅超管使用的接口地址集合
*/
private static final String[] SUPER_PERMISSION_PATH_ARR = {
"/auth/session/**",
"/auth/third/page",
"/client/user/**",
};
/**
* 注册 [Sa-Token 全局过滤器]
*/
@Bean
public SaServletFilter getSaServletFilter() {
return new SaServletFilter()
// 指定拦截路由
.addInclude("/**")
// 设置鉴权的接口
.setAuth(r -> {
SaRouter.match("/**")
.notMatch(CollectionUtil.newArrayList(NO_LOGIN_PATH_ARR))
.check(r1 -> StpUtil.checkLogin());
SaRouter.match(CollectionUtil.newArrayList(SUPER_PERMISSION_PATH_ARR))
.notMatch(CollectionUtil.newArrayList(NO_LOGIN_PATH_ARR))
.check(r1 -> StpUtil.checkRole("superAdmin"));
})
// 前置函数:在每次认证函数之前执行
.setBeforeAuth(obj -> {
// ---------- 设置跨域响应头 ----------
SaHolder.getResponse()
// 是否可以在iframe显示视图: DENY=不可以 | SAMEORIGIN=同域下可以 | ALLOW-FROM uri=指定域名下可以
// .setHeader("X-Frame-Options", "SAMEORIGIN")
// 是否启用浏览器默认XSS防护: 0=禁用 | 1=启用 | 1; mode=block 启用, 并在检查到XSS攻击时,停止渲染页面
.setHeader("X-XSS-Protection", "1; mode=block")
// 禁用浏览器内容嗅探
.setHeader("X-Content-Type-Options", "nosniff")
// 允许指定域访问跨域资源
.setHeader("Access-Control-Allow-Origin", "*")
// 允许所有请求方式
.setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE")
// 有效时间
.setHeader("Access-Control-Max-Age", "3600")
// 允许的header参数
.setHeader("Access-Control-Allow-Headers", "*");
// 如果是预检请求,则立即返回到前端
SaRouter.match(SaHttpMethod.OPTIONS)
// OPTIONS预检请求,不做处理
.free(r -> {
})
.back();
})
// 异常处理
.setError(e -> {
// 由于过滤器中抛出的异常不进入全局异常处理,所以必须提供[异常处理函数]来处理[认证函数]里抛出的异常
// 在[异常处理函数]里的返回值,将作为字符串输出到前端,此处统一转为JSON输出前端
SaResponse saResponse = SaHolder.getResponse();
saResponse.setHeader(Header.CONTENT_TYPE.getValue(), ContentType.JSON + ";charset=" + CharsetUtil.UTF_8);
CommonResult<String> commonResult = GlobalExceptionUtil.getCommonResult((Exception) e);
saResponse.setStatus(commonResult.getCode() < ResultCode.HttpStatusCodeMax
? commonResult.getCode()
: HttpStatus.FORBIDDEN.value());
return commonResult;
});
}
}
cn.dev33.satoken.filter.SaServletFilter#doFilter
在过滤中调用上面beforeAuth,Auth两个策略
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
try {
// 执行全局过滤器
SaRouter.match(includeList).notMatch(excludeList).check(r -> {
beforeAuth.run(null);
auth.run(null);
});
} catch (StopMatchException e) {
} catch (Throwable e) {
// 1. 获取异常处理策略结果
String result = (e instanceof BackResultException) ? e.getMessage() : String.valueOf(error.run(e));
// 2. 写入输出流
if(response.getContentType() == null) {
response.setContentType("text/plain; charset=utf-8");
}
response.getWriter().print(result);
return;
}
// 执行
chain.doFilter(request, response);
}
在auth策略中调用StpUtil.checkLogin()检验账号是否登录,底层就是拿前端的token去缓存查询登录凭证。
四.获取权限
1.获取权限在第三方框架snowy中比较简单,因为缓存中已经存有token和session的映射(session中存有用户信息),直接通过token就能在缓存中取到了。
vip.xiaonuo.auth.core.util.StpLoginUserUtil#getLoginUser中调用StpUtil.getTokenSession()会在缓存中取session信息
2.开启权限注解后(配置了注解拦截器),调用com.wisdomcity.laian.river.config.SaTokenConfig.StpInterfaceImpl#getPermissionList的时机会在方法或类上添加SaCheckPermission.class注解后调用。
拦截器cn.dev33.satoken.interceptor.SaAnnotationInterceptor#preHandle
cn.dev33.satoken.strategy.SaStrategy#checkElementAnnotation
调用链