Spring Boot + MyBatisPlus + ElementUI 实现后台管理系统之 02-登录、注销、异常处理
AI 摘要
Sa-Token
Sa-Token 是一个轻量级 Java 权限认证框架,主要解决:登录认证、权限认证、单点登录、OAuth2.0、分布式 Session 会话、微服务网关鉴权等一系列权限相关问题。
Sa-Token 官网:https://sa-token.cc/
一体式项目的登录认证
传统的登录流程,登录成功后把用户信息写到 session 中了。
前后端分离项目的登录认证
在传统的一体式项目当中,登录鉴权可以使用 session 会话。但是在前后端分离项目中,没办法使用 session,cookie 又存在跨域问题,此时鉴权就成了最大的问题。
前后端分离的项目实现登录认证,后端接口登录成功后,可以形成一个 token,叫做口令给前端,前端每一次请求后端接口的时候,都会携带这个口令,如果这个口令的状态正常,即已经登录,如果这个口令的状态不正常,即未登录。
Sa-Token 的登录认证
修改 pom.xml,导入 Sa-Token 的依赖。
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-spring-boot-starter</artifactId>
<version>1.34.0</version>
</dependency>修改 application.properties 配置文件,配置 Sa-Token。
# Sa-Token配置
# token名称 (同时也是cookie名称)
sa-token.token-name=satoken
# token有效期,单位s 默认30天, -1代表永不过期
sa-token.timeout=2592000
# token临时有效期 (指定时间内无操作就视为token过期) 单位: 秒
sa-token.activity-timeout=-1
# 是否允许同一账号并发登录 (为true时允许一起登录, 为false时新登录挤掉旧登录)
sa-token.is-concurrent=false
# 在多人登录同一账号时,是否共用一个token (为true时所有登录共用一个token, 为false时每次登录新建一个token)
sa-token.is-share=false
# token风格
sa-token.token-style=uuid
# 是否输出操作日志
sa-token.is-log=false新建测试控制器 cn.duozai.ct.controller.TestController,实现 Sa-Token 的测试方法。
@GetMapping("/login")
public ResponseResult login() {
// 调用StpUtil进行登录
StpUtil.login(10001);
// 获取当前会话的token值
System.out.println(StpUtil.getTokenValue());
return ResponseResult.success("登录成功");
}可以参考 Sa-Token 的官方文档,详细了解登录认证的相关方法。
Sa-Token 自定义 token 风格
Sa-Token 默认的 token 生成策略是 uuid 风格,我们还可以将 token 生成设置为其他风格。
修改 application.properties 配置文件,配置 Sa-Token 的 token 风格。
sa-token.token-style=uuidSa-Token 整合 JWT
在实战过程中,token 的风格一般是 JWT。JWT 是一个开放的行业标准,它定义了一种简介的、自包含的协议格式,用于在通信双方传递 JSON 对象,传递的信息经过数字签名可以被验证和信任。JWT 可以使用 HMAC 算法或使用 RSA 的公钥/私钥对来签名,防止被篡改。
Sa-Token 整合 JWT,在官方文档中也提供了对应的介绍和说明。
修改 pom.xml,导入 Sa-Token-JWT 的依赖。
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-spring-boot-starter</artifactId>
<version>1.34.0</version>
</dependency>修改 application.properties 配置文件,配置 Sa-Token-JWT。
# token风格
# sa-token.token-style=uuid
# JWT密钥
sa-token.jwt-secret-key=86765768hjnkdjifgbjtrfef新建 Sa-Token 配置类 cn.duozai.ct.configure.SaTokenConfigure,配置 JWT。
@Configuration
public class SaTokenConfigure {
/**
* 整合JWT
*
* @author 多仔ヾ
*/
@Bean
public StpLogic getStpLogicJwt() {
return new StpLogicJwtForSimple();
}
}重启项目进行测试,查看输出的 token 的风格。
Sa-Token 整合 Redis
Sa-Toke 默认将数据保存在内存中,此模式读写速度最快,且避免了序列化与反序列化带来的性能消耗,但是此模式也有一些缺点,比如:重启后数据会丢失。无法在分布式环境中共享数据。为此,Sa-Token 提供了扩展接口,我们可以轻松将会话数据存储在 Redis、Memcached 等专业的缓存中间件中,做到重启数据不丢失,而且保证分布式环境下多节点的会话一致性。
Sa-Token 整合 Redis,在官方文档中也提供了对应的介绍和说明。
修改 pom.xml,导入 Sa-Token-Redis-Jackson 的依赖。
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-dao-redis-jackson</artifactId>
<version>1.34.0</version>
</dependency>重启项目进行测试,查看 Redis 数据库的数据变化。
可以在整合 Redis 之前测试重启项目后的登录情况:项目重启后,登录状态失效。
整合 Redis 之后:项目重启后,登录状态还在。
新建测试控制器 TestController,实现 Sa-Token 的测试方法。
@GetMapping("/login")
public ResponseResult login() {
// 验证是否登录
System.out.println("是否登录:" + StpUtil.isLogin())
// 调用StpUtil进行登录
StpUtil.login(10001);
// 获取当前会话的token值
System.out.println(StpUtil.getTokenValue());
return ResponseResult.success("登录成功");
}
@GetMapping("/getUser")
public ResponseResult getUser() {
if(StpUtil.isLogin()) {
return ResponseResult.success("用户信息");
} else {
return ResponseResult.failure("请先登录");
}
}实现后端路由拦截
项目中所有接口均需要登录认证,只有登录接口本身对外开放,此时可以借助 Sa-Token 路由拦截鉴权的功能。也就是说,在没有登录之前,无法访问其他功能接口。
路由拦截鉴权一共有两种方式,一种是拦截器,一种是过滤器。相比于拦截器,过滤器更加底层,执行时机更靠前,有利于防渗透扫描。过滤器可以拦截静态资源,方便我们做一些权限控制。推荐使用过滤器实现路由拦截鉴权。
修改 Sa-Token 配置类 cn.duozai.ct.configure.SaTokenConfigure,注册 Sa-Token 全局过滤器。
/**
* 注册Sa-Token全局过滤器
*
* @author 多仔ヾ
* @return cn.dev33.satoken.filter.SaServletFilter
*/
@Bean
public SaServletFilter getSaServletFilter() {
return new SaServletFilter()
// 指定拦截路由与放行路由
.addInclude("/**").addExclude("/favicon.ico")
// 认证函数:每次请求执行
.setAuth(obj -> {
// 登录认证:拦截所有路由,并排除/api/auth/login用于开放登录
SaRouter.match("/**", "/api/auth/login", () -> StpUtil.checkLogin());
})
// 异常处理函数:每次认证函数发生异常时执行此函数
.setError(e -> {
// 设置响应头
SaHolder.getResponse().setHeader("Content-Type", "application/json;charset=UTF-8");
// 使用封装的 JSON 工具类转换数据格式
return JSONUtil.toJsonStr(ResponseResult.resp(ResponseStatus.OF_LOGINED.code, ResponseStatus.OF_LOGINED.message));
})
// 前置函数:在每次认证函数之前执行
.setBeforeAuth(obj -> {
// 设置跨域响应头
SaHolder.getResponse()
// 允许指定域访问跨域资源
.setHeader("Access-Control-Allow-Origin", "*")
// 允许的header参数
.setHeader("Access-Control-Allow-Headers", "*")
// 允许所有请求方式
.setHeader("Access-Control-Allow-Methods", "*");
});
}运行测试,在未登录时访问其他接口,会返回 401。登录成功后,即可正常访问其他接口。
实现全局异常处理
用户在访问一个不存在的接口时,会直接出现“Whitelabel Error Page”,很不美观。包括我们的程序当中出现异常时,也是直接抛出 500,出现 500 页面。在实际开发当中,我们不建议直接抛出异常,而是要对这些异常进行处理。如:当出现 404 时,我们返回友好的 404 提示。
新建统一异常处理类 cn.duozai.ct.utils.exception.GlobalExceptionHandler,捕获并处理异常。
/**
* 统一异常处理类
*
* @author 多仔ヾ
*/
@RestControllerAdvice
public class GlobalExceptionHandler {
/**
* NoHandlerFoundException异常处理(404)
*
* @author 多仔ヾ
* @param ex NoHandlerFoundException
* @return cn.duozai.ct.utils.response.ResponseResult
*/
@ExceptionHandler(NoHandlerFoundException.class)
public ResponseResult exceptionHandler(NoHandlerFoundException ex) {
return ResponseResult.resp(ResponseStatus.NOT_FOUND.code, ResponseStatus.NOT_FOUND.message);
}
/**
* Exception异常处理(500)
*
* @author 多仔ヾ
* @param ex Exception
* @return cn.duozai.ct.utils.response.ResponseResult
*/
@ExceptionHandler(value = Exception.class)
public ResponseResult handler(Exception ex) {
return ResponseResult.error();
}
}@RestControllerAdvice 用于标记统一异常处理类,@ExceptionHandler 用于指定异常处理。
要统一处理 404,还需要修改配置文件 application.properties。
# 统一异常处理
spring.mvc.throw-exception-if-no-handler-found=true
# 禁用资源映射
spring.web.resources.add-mappings=false实现登录接口
新建权限认证控制器 cn.duozai.ct.controller.AuthController,实现登录接口。
/**
* 权限认证控制器
*
* @author 多仔ヾ
*/
@RequestMapping("/api/auth")
@RestController
public class AuthController {
@Resource
SysUsersService sysUsersService;
/**
* 系统用户登录
*
* @author 多仔ヾ
* @param account 账户账号
* @param password 账户密码
* @return cn.duozai.ct.utils.response.ResponseResult
*/
@PostMapping("/login")
public ResponseResult login(@RequestParam(name = "account") String account,
@RequestParam(name = "password") String password) {
// 根据账户账号查询系统用户信息
SysUsers sysUser = sysUsersService.getOne(new QueryWrapper<SysUsers>().eq("account", account));
// 判断账户账号是否存在
if(sysUser == null) {
return ResponseResult.failure("该账户账号不存在!");
}
// 判断密码是否正确
String md5SaltsPassword = MD5Util.encode(password, sysUser.getSalts());
if(!md5SaltsPassword.equals(sysUser.getPassword())) {
return ResponseResult.failure("账户密码错误!");
}
// 判断账户状态是否启用
if(sysUser.getStatus() != 1) {
return ResponseResult.failure("该账户已被禁用!");
}
// 登录成功,调用StpUtil进行登录
StpUtil.login(sysUser.getId());
// 返回成功结果 + token令牌
return ResponseResult.success("登录成功", MapUtil.builder().put("token", StpUtil.getTokenValue()).build());
}
}使用 Apifox 完善文档,并进行接口测试。
实现注销接口
在权限认证控制器 cn.duozai.ct.controller.AuthController 中实现注销接口。
/**
* 系统用户注销
*
* @author 多仔ヾ
* @return cn.duozai.ct.utils.response.ResponseResult
*/
@GetMapping("/logout")
public ResponseResult logout() {
StpUtil.logout();
return ResponseResult.success("注销成功");
}使用 Apifox 完善文档,并进行接口测试。