其实一直想写一个单点登录系统,现在的现状是公司内部有非常多项目的,然后每个项目一套登录系统,系统和系统之间存在单独的鉴权,每一个操作应用都需要登录一次的话,这样不仅仅用户体验不好,也会出现非常多重复的代码,于是单点登录的需求诞生了。
轻量级且开箱即用,如果你说为何不用shiro,那么我只能说它比shiro更加轻量更加好用,只需要简单的配置和写几行代码,即可实现登录功能。
1.往项目中引入sa-token依赖
cn.dev33 sa-token-spring-boot-starter 1.34.0 cn.dev33 sa-token-sso 1.34.0 cn.dev33 sa-token-dao-redis-jackson 1.34.0
2.编写配置文件
# 端口
server:port: 9000# Sa-Token 配置
sa-token: # ------- SSO-模式一相关配置 (非模式一不需要配置) # cookie:# 配置 Cookie 作用域 # domain: stp.com# ------- SSO-模式二相关配置 sso: # Ticket有效期 (单位: 秒),默认五分钟 ticket-timeout: 300# 所有允许的授权回调地址allow-url: "*"# 是否打开单点注销功能is-slo: true# ------- SSO-模式三相关配置 (下面的配置在SSO模式三并且 is-slo=true 时打开) # 是否打开模式三 isHttp: true# 接口调用秘钥(用于SSO模式三的单点注销功能)secretkey: kQwIOrYvnXmSDkwEiFngrKidMcdrgKor# ---- 除了以上配置项,你还需要为 Sa-Token 配置http请求处理器(文档有步骤说明) spring: # Redis配置 (SSO模式一和模式二使用Redis来同步会话)redis:# Redis数据库索引(默认为0)database: 1# Redis服务器地址host: 127.0.0.1# Redis服务器连接端口port: 6379# Redis服务器连接密码(默认为空)password: # 连接超时时间timeout: 10slettuce:pool:# 连接池最大连接数max-active: 200# 连接池最大阻塞等待时间(使用负值表示没有限制)max-wait: -1ms# 连接池中的最大空闲连接max-idle: 10# 连接池中的最小空闲连接min-idle: 0
3.编写登录接口
// 会话登录接口
@RequestMapping("doLogin")
public SaResult doLogin(String name, String pwd) {// 第一步:比对前端提交的账号名称、密码if("zhang".equals(name) && "123456".equals(pwd)) {// 第二步:根据账号id,进行登录 StpUtil.login(10001);return SaResult.ok("登录成功");}return SaResult.error("登录失败");
}
上术代码已经实现了登录鉴权,但是我们也许可以注意到此处仅仅做了会话登录,并没有主动向前端返回 Token 信息。 是因为不需要吗?严格来讲是需要的,只不过
StpUtil.login(id)方法利用了Cookie自动注入的特性,省略了你手写返回 Token 的代码。
但在某些情况下,我们就是需要返回token给到前端,那应该如何操作呢?请往下看
登录接口返回token信息
// 登录接口@RequestMapping("doLogin")public SaResult doLogin(String username, String password) {// 第1步,先登录上StpUtil.login(10003);// 第2步,获取 Token 相关参数SaTokenInfo tokenInfo = StpUtil.getTokenInfo();// 第3步,返回给前端return SaResult.data(tokenInfo);}
接口将会返回如下的数据:

其中
tokenName是请求头名称,tokenValue是请求头的值,将它们放入请求头即可保持登录态。
请求头放入token【登录接口返回】即可,比如:

controller代码如下:
@RequestMapping("/userinfo")public Object userinfo() {// 自定义返回结果(模拟)return SaResult.ok().set("id", StpUtil.getLoginId()).set("name", "zengjq").set("sex", "男").set("age", 18);}
是不是非常简单。我们通过
StpUtil.getLoginId()即可拿到登录用户的id,但实际情况下我们往往需要拿到用户名称,部门之类的更多信息,而登录态工具类StpUtil并未不能获取到,有同学肯定想到了,我拿用户id去访问数据库呀,但是每次都去查数据库那么它的压力就太大啦,其实sa-token提供了一个TokenSession。
它是会话中的数据缓存组件,通过Session我们可以很方便的缓存一些高频读写数据,提高程序性能,例如:
// 在登录时缓存user对象
StpUtil.getTokenSession().set("user", user);// 然后我们就可以在任意处使用这个user对象
SysUser user = (SysUser) StpUtil.getTokenSession().get("user");
其实Sa-Token还提供了别的session类,这里不做赘述,有兴趣可以移步官方文档 sa-token session
我们来看拦截器代码:
import cn.dev33.satoken.interceptor.SaInterceptor;
import cn.dev33.satoken.stp.StpUtil;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;@Configuration
public class SaTokenConfigure implements WebMvcConfigurer {// 注册拦截器@Overridepublic void addInterceptors(InterceptorRegistry registry) {// 新增登录校验拦截器 校验规则为 StpUtil.checkLogin() 登录校验。SaInterceptor saInterceptor = new SaInterceptor(handle -> StpUtil.checkLogin());// 注册 Sa-Token 拦截器registry.addInterceptor(saInterceptor).addPathPatterns("/**").excludePathPatterns("/user/doLogin", "/demo/**"); // 排除了/user/doLogin接口用来开放登录}
}
这里的拦截器使用的是
spring框架自带的,并不是sa-token提供的,所以它不仅仅可以用于sa-token的鉴权,还可以应用于spring框架的所有拦截操作
实现完上述代码,sa-token 项目的简单鉴权就算是完成了。但其实sa-token还提供了许多功能,比如权限认证、OAuth2.0、分布式Session会话、微服务网关鉴权 等等,有需要还是建议使用前看看官方文档 Sa-Token
Sa-Token 我的理解是:它会更加希望你搭配一套统一的前端来使用,而不是每个系统都搭建一套登录系统,就算是在SSO单点登录模块里面,它依旧会给出 SSO整合-定制化登录页面 的教程,希望在未登录时跳转至我们编写好的页面,而不是去返回统一状态码给到前端。而我们公司会更加倾向于去返回统一json数据和不同的code状态码,其他交给前端自行判断。我们来看看我公司的处理。
如果我想未登录时返回某个状态码,我们需要在异常处理类中加入NotLoginException的handler
异常处理类代码如下:
@ControllerAdvice
public class GlobalExceptionHandler {private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class);@ExceptionHandler(NotLoginException.class)@ResponseStatus(HttpStatus.OK) // HttpStatus.OK是因为前端不基于httpStatus去判断接口是否异常@ResponseBodypublic SaResult handleNoHandlerFoundException(NotLoginException e) {log.error("用户未登录 ", e);// ErrorCode.LOGIN_REQUIRE.getCode() 是未登录时的与前端约定好的异常编码return SaResult.error(e.getMessage()).setCode(ErrorCode.LOGIN_REQUIRE.getCode());}@ExceptionHandler(SaTokenException.class)@ResponseStatus(HttpStatus.OK)@ResponseBodypublic SaResult handleSaTokenException(SaTokenException e) {log.error("sa-token抛出其他异常 ", e);return SaResult.error(e.getMessage());}
}
当接口404时,spring会拦截后直接抛出异常信息:

上面的json其实并不是我们返回的,而是spring框架层返回的,如果我想改成自定义json,应该怎么处理呢?请往下看
自定义接口404响应信息
1、在配置文件中加入配置
spring.mvc.throw-exception-if-no-handler-found=true
spring.resources.add-mappings=false
throw-exception-if-no-handler-found表示当没有对应处理器时,允许抛出异常;而add-mappings表示是否为静态资源添加对应的处理器。而默认404异常不被全局处理器拦截,才导致未抛出异常。
2、在全局异常处理中捕获404
@ExceptionHandler(NoHandlerFoundException.class)@ResponseStatus(HttpStatus.OK)@ResponseBodypublic SaResult handle404Error(NoHandlerFoundException e) {log.error("访问资源不存在", e);return new SaResult(404, "访问的资源不存在", null);}
其实一般我们会直接捕获Exception.class而不是特定的异常,只是如果你想做特殊处理,才需要类似上述的处理代码
好了,以上就是本次博客的全部内容,如有疑问,欢迎留言沟通