步骤 1 什么是sa-token
我是偶然间在知乎发现了这个框架,是国人写的,还不错,就用了。
官网:https://sa-token.dev33.cn
Sa-Token 是一个轻量级 Java 权限认证框架,主要解决:登录认证、权限认证、Session会话、单点登录、OAuth2.0、微服务网关鉴权 等一系列权限相关问题。
步骤 2 pom.xml
<!-- Sa-Token 权限认证, 在线文档:http://sa-token.dev33.cn/ --> <dependency> <groupId>cn.dev33</groupId> <artifactId>sa-token-spring-boot-starter</artifactId> <version>1.24.0</version> </dependency>
导入依赖即可
步骤 3 登录的时候怎么用sa-token的,什么原理?
看下登录方法:
/** * 用户 登录 * @param user * @return */ @PostMapping("login.do") public Result login(User user){ //先判断用户名是否存在 User userReal = null; if((userReal = service.getUserByUsername(user.getUserName())) == null){ throw new BizException(ExceptionCodeEnum.ERROR_PARAM.setDesc("用户名不存在!")); } //再判断密码是否正确 if(!userReal.getPassword().equals(user.getPassword())){ throw new BizException(ExceptionCodeEnum.ERROR_PARAM.setDesc("密码错误!")); } StpUtil.login(userReal.getId()); return Result.success(); }
逻辑:查询username存不存在,再查询密码是否正确,只要没有抛异常就调用sa-token的login方法。
StpUtil.login(userReal.getId());
login方法传入我们user的id,比如:
把int类型的数值传进去了。
步骤 4 sa-token登录认证
核心思想
所谓登录认证,说白了就是限制某些API接口必须登录后才能访问(例:查询我的账号资料)
那么如何判断一个会话是否登录?框架会在登录成功后给你做个标记,每次登录认证时校验这个标记,有标记者视为已登录,无标记者视为未登录!
所以,只要登录成功,我们就用
StpUtil.login(userReal.getId());
记录了当前用户的登录id
步骤 5 StpUtil.login(userReal.getId())的秘密
我们如果不使用sa-token,怎么做登录功能呢?
是不是需要在传参的时候加一个HttpServletRequest,然后再用getSession方法获取session,把登录用户的信息放到session中?
而现在,只需要一句StpUtil.login(userReal.getId())就维持了登录状态,想也知道sa-token框架肯定也是把登录id放到session中了。因为没有用redis,所以要维持登录肯定是用了session或cookie。
如果是第一次登录,就生成tokenValue
然后把生成的token写入storage([存储器] 包装类SaStorage),
1. // 在当前会话写入当前tokenValue 2. setTokenValue(tokenValue, loginModel.getCookieTimeout());
setTokenValue方法:
/** * 在当前会话写入当前TokenValue * @param tokenValue token值 * @param cookieTimeout Cookie存活时间(秒) */ public void setTokenValue(String tokenValue, int cookieTimeout){ SaTokenConfig config = getConfig(); // 将token保存到[存储器]里 SaStorage storage = SaHolder.getStorage(); // 判断是否配置了token前缀 String tokenPrefix = config.getTokenPrefix(); if(SaFoxUtil.isEmpty(tokenPrefix)) { storage.set(splicingKeyJustCreatedSave(), tokenValue); } else { // 如果配置了token前缀,则拼接上前缀一起写入 storage.set(splicingKeyJustCreatedSave(), tokenPrefix + SaTokenConsts.TOKEN_CONNECTOR_CHAT + tokenValue); } // 注入Cookie if(config.getIsReadCookie()){ SaResponse response = SaHolder.getResponse(); response.addCookie(getTokenName(), tokenValue, "/", config.getCookieDomain(), cookieTimeout); } }
SaStorage是一个接口,set方法是把token存入request对象中。
关键是下面一段:
esponse.addCookie(getTokenName(), tokenValue, "/", config.getCookieDomain(), cookieTimeout);
这句话利用cookie保存了当前登录用户的token。
谷歌浏览器查看cookie方式:右上角有三个点的按钮 - 设置
搜索localhost,找到satoken,这就是上面代码中getTokenName()方法的返回值
步骤 6 sa-token为什么能获取到response对象?SaHolder的秘密。。。(深挖,看不懂就跳过,没事)
之前我也一直想不通一个问题,sa-token用起来也太方便了吧,就这么一句话,什么都搞定了。我也不需要去关心session,也不要管HttpServletResponse啥的。
秘密就在这:
SaResponse response = SaHolder.getResponse();
SaHolder调用getResponse方法得到SaResponse, 这个SaResponse是一个接口
package cn.dev33.satoken.context.model; /** * Response 包装类 * @author kong * */ public interface SaResponse { /** * 获取底层源对象 * @return see note */ public Object getSource(); /** * 删除指定Cookie * @param name Cookie名称 */ public void deleteCookie(String name); /** * 写入指定Cookie * @param name Cookie名称 * @param value Cookie值 * @param path Cookie路径 * @param domain Cookie的作用域 * @param timeout 过期时间 (秒) */ public void addCookie(String name, String value, String path, String domain, int timeout); /** * 在响应头里写入一个值 * @param name 名字 * @param value 值 * @return 对象自身 */ public SaResponse setHeader(String name, String value); /** * 在响应头写入 [Server] 服务器名称 * @param value 服务器名称 * @return 对象自身 */ public default SaResponse setServer(String value) { return this.setHeader("Server", value); } /** * 重定向 * @param url 重定向地址 * @return 任意值 */ public Object redirect(String url); }
我们目前只用了addCookie方法,然后再看SaResponse的实现类
只有一个实现类,addCookie方法如下:
/** * 写入指定Cookie */ @Override public void addCookie(String name, String value, String path, String domain, int timeout) { Cookie cookie = new Cookie(name, value); if(SaFoxUtil.isEmpty(path) == true) { path = "/"; } if(SaFoxUtil.isEmpty(domain) == false) { cookie.setDomain(domain); } cookie.setPath(path); cookie.setMaxAge(timeout); response.addCookie(cookie); }
和我们预期的是一致的。
现在的问题是,SaHolder究竟是怎么getResponse的?
代码如下:
public static SaResponse getResponse() { return SaManager.getSaTokenContext().getResponse(); }
原来是saTokenContext的绝活,再看SaManager的getSaTokenContext方法:
public static SaTokenContext getSaTokenContext() { if (saTokenContext == null) { synchronized (SaManager.class) { if (saTokenContext == null) { setSaTokenContext(new SaTokenContextDefaultImpl()); } } } return saTokenContext; }
我以为秘密在 new SaTokenContextDefaultImpl() 里面。
SaTokenContextDefaultImpl是Sa-Token 上下文处理器 [默认实现类]。
结果一看代码,懵逼了:
敢情这是在嘲讽我吗,这估计是不正常的情况下才会走到这吧,肯定不是。那如果不是用的SaTokenContextDefaultImpl,难道saTokenContext本来就有值的?
saTokenContext是一个接口,有三个实现类:
因为这是SpringBoot项目怎么看也像是SaTokenContextForSpring
重新登录看看,发现
这个方法返回了SaResponseForServlet对象,属于SaResponse的实现类,所以在上面SaHolder的getResponse方法获取的实际是SaResponseForServlet对象。又是多态,多态好是好,封装了底层的实现。只给出接口类的简单操作,就是如果翻源码会有点麻烦,需要一层层地找。
SaResponseForServlet的实例化代码如下
那么关键就在于SpringMVCUtil是怎么getResponse的?
关键就在于RequestContextHolder了,它调用了getRequestAttributes方法,得到 servletRequestAttributes对象,查看数据发现
哦,到这一步就有点豁然开朗了,response的值是:
com.alibaba.druid.support.http.WebStatFilter$StatHttpServletResponseWrapper@3cca73f3
至于这个玩意又是什么东东,老实说,我目前还没有完全搞明白,目前只知道这个类的位置是在druid的jar包里面: