springboot集成sa-token来实现登录鉴权(一)
创始人
2025-05-28 16:11:32

springboot集成sa-token来实现登录鉴权(一)

  • springboot集成sa-token来实现登录鉴权(一)
    • sa-token简介
    • spirngboot集成sa-token代码实现
      • maven依赖
      • 配置文件
      • 全局异常处理
      • 登录相关接口
        • controller层
        • service层
        • 接口中的参数类
        • 返回数据封装
        • 全局变量类和枚举类
    • 测试演示
      • 首先获取一个临时token
      • 然后获取验证码图片
      • 其次是真正的登录接口调用
      • 其他接口,调用的时候会校验是否登录

springboot集成sa-token来实现登录鉴权(一)

这篇文章,我们将学习怎样初步集成sa-token来实现基本的登录认证和权限校验,并初步看下底层的源码实现。

sa-token简介

sa-token是一个轻量级的登录鉴权框架,比之前的shiro和springsecurity都要轻量,一行代码就可以实现登录功能。具体文档可以看:sa-token官网。这里我们不再赘述。直接来看具体实现代码。

spirngboot集成sa-token代码实现

点击看项目源码

maven依赖

		cn.dev33sa-token-spring-boot-starter1.34.0cn.dev33sa-token-dao-redis-jackson1.34.0org.apache.commonscommons-pool2

其中,如果是springboot3.x版本的话,上面的sa-token-spring-boot-starter依赖要换成sa-token-spring-boot3-starter

配置文件

@Configuration
public class SaTokenConfigure implements WebMvcConfigurer {@Overridepublic void addInterceptors(InterceptorRegistry registry) {registry.addInterceptor(new SaInterceptor(handler -> {SaRouter.match("/**", StpUtil::checkLogin).notMatch(SaTokenConstant.excludePathPatterns);// 这里可以给每个接口地址的路径鉴权,当然,也可以用注解在每个接口上鉴权})).addPathPatterns(SaTokenConstant.allRouters).excludePathPatterns(SaTokenConstant.excludePathPatterns);}@Bean@Primarypublic SaTokenConfig getSaTokenConfigPrimary(){SaTokenConfig config = new SaTokenConfig();// token名称config.setTokenName(SaTokenConstant.TOKEN_NAME);// token有效期,单位 秒,默认是30天config.setTimeout(30 * 24 * 60 * 60);// token无操作存活时间(指定时间内无操作就视为token过期) 单位:秒config.setActivityTimeout(SaTokenConstant.ACTIVITY_TIMEOUT);// 是否允许同一账号并发登录,true是允许多个同一账号一起登录,false时同一账号新登录的会挤掉旧登录的config.setIsConcurrent(false);// 在多人登录同一账号时,是否共用一个tokenconfig.setIsShare(true);// token风格config.setTokenStyle(TokenStyleEnum.SIMPLE_UUID.getTokenStyle());// 是否输出操作日志config.setIsLog(false);/*是否尝试从 cookie 里读取 Token,此值为 false 后,StpUtil.login(id) 登录时也不会再往前端注入Cookie,false时,可以返回给前端token,然后让前端在每次请求时header里携带返回的token,这样可以用于不能使用cookie的设备端,不在局限于web浏览器端*/config.setIsReadCookie(false);return config;}
}

全局异常处理

/*** 全局异常处理*/
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {/*** 全局登录异常处理* @param exception 登录抛出的异常* @return 提示信息*/@ExceptionHandler(NotLoginException.class)public BaseResult handlerNotLoginException(NotLoginException exception) {// 打印日志log.error(exception.getMessage(), exception);// 判断登录异常的场景,定制化返回提示String type = exception.getType();switch (type){case NotLoginException.INVALID_TOKEN:case NotLoginException.TOKEN_TIMEOUT:return BaseResult.fail(520, SaTokenConstant.TOKEN_OVERDUE);case NotLoginException.BE_REPLACED:case NotLoginException.KICK_OUT:return BaseResult.fail(520, SaTokenConstant.LOGIN_REPLACE);default:return BaseResult.fail(520, SaTokenConstant.NOT_TOKEN);}}/*** 全局权限校验异常处理* @param exception 权限校验异常* @return*/@ExceptionHandler(NotPermissionException.class)public BaseResult handlerNotPermissionException(NotPermissionException exception){// 打印日志log.error(exception.getMessage(), exception);return BaseResult.fail(520, SaTokenConstant.PERMISSION_ERROR);}
}
 

登录相关接口

这里我们模拟下真实的登录场景:
用户在登录页一般会看到三个输入框,分别是:账号、密码和验证码图片。那么我们首先要在登录页面获取到验证码图片,并在登录接口中传入验证码并进行校验,那么后端就必须要存下返回给前端的验证码,那么我们需要先返回前端一个临时token,然后前端拿着这个临时token来请求获取验证码的接口,后端拿到这个临时token后,生成验证码,并将这个临时token作为缓存的key,验证码内容作为value存入缓存中。等到前端调用真正的登录接口时,需要传入 账号、密码、验证码和临时token,在这个接口中,我们通过临时token,去缓存中拿到验证码,和前端传入的验证码进行比对,然后再校验账号和密码。

controller层

@RestController
@Slf4j
public class LoginController {@Resourceprivate VerifyImgUtil verifyImgUtil;@Resourceprivate LoginService loginService;/*** 在获取验证码接口和登录接口前,先调用,获取一个临时token,用来作为验证码的key** @return*/@GetMapping("tempToken")public BaseResult getTempToken(@RequestParam("tokenKey") String tokenKey) {log.info("开始获取临时token------>" + tokenKey);String token = SaTempUtil.createToken(tokenKey, 5 * 60);loginService.saveTempToken(token);return BaseResult.success(token, null);}/*** 获取图片验证码** @param tempToken 获取到的临时token* @param response*/@GetMapping("getVerifyImg")public BaseResult getVerifyImg(@RequestParam("tempToken") String tempToken, HttpServletResponse response) throws IOException {log.info("开始获取图片验证码------>" + tempToken);if (!loginService.checkTempToken(tempToken)) {return BaseResult.fail(530, LoginConstant.TOKEN_EMPTY);}response.setContentType("image/jpeg");//设置相应类型,告诉浏览器输出的内容为图片response.setHeader("Pragma", "No-cache");//设置响应头信息,告诉浏览器不要缓存此内容response.setHeader("Cache-Control", "no-cache");response.setDateHeader("Expires", 0);OutputStream os = response.getOutputStream();verifyImgUtil.lineCaptcha(os, tempToken);try {os.flush();} finally {os.close();}return null;}/*** 模拟登录接口,真实场景中,可以先提供一个接口,返回一个临时token,* 然后再提供一个获取验证码的接口,里面需要传入刚才返回的临时token,生成验证码后,以临时token作为key,验证码作为value放入缓存,* 然后再来调用这个正式登录接口,里面需要传验证码和临时token以及用户的账号密码,先根据临时token从缓存中获取验证码来比对,* 然后再比对账号密码,比对成功后,删除缓存中对应临时token的验证码数据,并删除临时token** @param dto 登录数据* @return*/@PostMapping("/login")public BaseResult login(@RequestBody LoginDto dto) {log.info("开始登录------>" + JSON.toJSONString(dto));BaseResult result = loginService.checkLogin(dto);if (!result.isState()) {return result;}loginService.deleteTempToken(dto.getTempToken());// 校验成功后移除临时token// 登录StpUtil.login("10001");// 将用户的数据放入上下文session中StpUtil.getSession().set(LoginConstant.LOGIN_NAME, "admin");StpUtil.getSession().set(LoginConstant.REAL_NAME, "管理员");StpUtil.getSession().set(LoginConstant.USER_ID, "10001");return BaseResult.success(StpUtil.getTokenValue(), "登录成功");}/*** 注销登录接口*/@PostMapping("logout")public void logout(){StpUtil.logout();}
}

service层

public interface LoginService {/*** 登录校验* @param dto* @return*/BaseResult checkLogin(LoginDto dto);/*** 存放临时token* @param tempToken 临时token*/void saveTempToken(String tempToken);/*** 删除临时token* @param tempToken 临时token*/void deleteTempToken(String tempToken);/*** 校验临时token是否有效* @param tempToken 临时token* @return*/boolean checkTempToken(String tempToken);
}
@Service
public class LoginServiceImpl implements LoginService {@Resourceprivate RedisUtil redisUtil;private static final String tempTokenPre = "tempToken";@Overridepublic BaseResult checkLogin(LoginDto dto) {if (StringUtils.isBlank(dto.getTempToken())) {return BaseResult.fail(530, LoginConstant.TOKEN_EMPTY);}if (StringUtils.isBlank(dto.getVerifyCode())) {return BaseResult.fail(530, LoginConstant.VERIFY_CODE_EMPTY);}if (StringUtils.isBlank(dto.getLoginName())) {return BaseResult.fail(530, LoginConstant.LOGIN_NAME_EMPTY);}if (StringUtils.isBlank(dto.getPassword())) {return BaseResult.fail(530, LoginConstant.PASSWORD_EMPTY);}// 校验Object verifyCode = redisUtil.get(dto.getTempToken());if (verifyCode == null) {return BaseResult.fail(530, LoginConstant.VERIFY_OVERDUE);}if (!dto.getVerifyCode().equalsIgnoreCase(verifyCode.toString())) {return BaseResult.fail(530, LoginConstant.VERIFY_CODE_ERROR);}if (!"admin".equals(dto.getLoginName())) {return BaseResult.fail(530, LoginConstant.LOGIN_NAME_ERROR);}if (!"123".equals(dto.getPassword())) {return BaseResult.fail(530, LoginConstant.PASSWORD_ERROR);}// 校验成功后,移除redisredisUtil.delete(dto.getTempToken());return BaseResult.success(null, null);}@Overridepublic void saveTempToken(String tempToken) {redisUtil.set(tempTokenPre + tempToken, "1", 300);}@Overridepublic void deleteTempToken(String tempToken) {redisUtil.delete(tempTokenPre + tempToken);}@Overridepublic boolean checkTempToken(String tempToken) {Object o = redisUtil.get(tempTokenPre + tempToken);return o != null;}
}

接口中的参数类

@Data
public class LoginDto implements Serializable {/*** 账号*/private String loginName;/*** 密码*/private String password;/*** 验证码*/private String verifyCode;/*** 临时token,用来校验验证码*/private String tempToken;
}

返回数据封装

@Data
public class BaseResult implements Serializable {/*** 返回数据*/private T data;/*** 提示信息*/private String message;/*** 状态码*/private int code;/*** 状态*/private boolean state = true;public static  BaseResult result(int code, String message, boolean state) {BaseResult result = new BaseResult<>();result.setCode(code);result.setMessage(message);result.setState(state);return result;}/*** 返回信息** @param code    状态码* @param message 信息* @param t       数据* @param      T* @return ResultVo*/public static  BaseResult result(int code, String message, T t, boolean state) {BaseResult r = new BaseResult<>();r.setCode(code);r.setMessage(message);r.setData(t);r.setState(state);return r;}/*** 返回成功** @param data data* @param   T* @return ResultVo*/public static  BaseResult success(T data, String message) {return result(HttpStatus.OK.value(), message, data, true);}/*** 返回失败** @param code code* @param   T* @return ResultVo*/public static  BaseResult fail(int code, String message) {return result(code, message, null, false);}
}

全局变量类和枚举类

/*** 要排除登录校验的接口地址枚举类*/
@Getter
public enum ExcludePathEnum {TEMP_TOKEN("/tempToken", "获取临时token的接口"),VERIFY_IMG("/getVerifyImg", "获取验证码图片的接口"),LOGIN("/login", "登录接口"),LOGOUT("/logout", "注销登录接口");private final String path;private final String remark;ExcludePathEnum(String path, String remark) {this.path = path;this.remark = remark;}
}
public class LoginConstant {public static final String TOKEN_EMPTY = "token不能为空!";public static final String LOGIN_NAME_EMPTY = "请输入账号!";public static final String PASSWORD_EMPTY = "请输入密码!";public static final String LOGIN_NAME_ERROR = "账号不存在!";public static final String PASSWORD_ERROR = "密码错误,请重新输入!";public static final String VERIFY_CODE_EMPTY = "验证码不能为空!";public static final String VERIFY_CODE_ERROR = "验证码错误,请重新输入!";public static final String VERIFY_OVERDUE = "验证码已过期,请重新登录!";public static final String LOGIN_NAME = "loginName";public static final String REAL_NAME = "userName";public static final String USER_ID = "userId";
}
public class SaTokenConstant {/*** token的字段名*/public static final String TOKEN_NAME = "token";/*** token无操作存活时间(指定时间内无操作就视为token过期) 单位:秒*/public static final long ACTIVITY_TIMEOUT = 24 * 60 * 60;public static final List allRouters = Collections.singletonList("/**");public static final List excludePathPatterns = Arrays.asList("/test/**","/file/upload/img/head/**","/swagger-resources/**","/webjars/**","/v2/**","/doc.html","**/swagger-ui.html","/swagger-ui.html/**","/img/head/**",ExcludePathEnum.TEMP_TOKEN.getPath(),ExcludePathEnum.VERIFY_IMG.getPath(),ExcludePathEnum.LOGIN.getPath(),ExcludePathEnum.LOGOUT.getPath());public static final String NOT_TOKEN = "您未登录,请登录!";public static final String TOKEN_OVERDUE = "登录已失效,请重新登录!";public static final String LOGIN_REPLACE = "您的账号已在别处登录!";public static final String PERMISSION_ERROR = "您没有权限!";
}
@Getter
public enum TokenStyleEnum {/*** 一般的uuid风格*/UUID("uuid"),/*** uuid风格,只不过去掉了中划线*/SIMPLE_UUID("simple-uuid"),/*** 随机32位字符串*/RANDOM_32("random-32"),/*** 随机64位字符串*/RANDOM_64("random-64"),/*** 随机128位字符串*/RANDOM_128("random-128"),/*** tik风格,gr_SwoIN0MC1ewxHX_vfCW3BothWDZMMtx__*/TIK("tik");private final String tokenStyle;TokenStyleEnum(String tokenStyle) {this.tokenStyle = tokenStyle;}
}

测试演示

首先获取一个临时token

在这里插入图片描述

然后获取验证码图片

在这里插入图片描述

其次是真正的登录接口调用

在这里插入图片描述
这里获取到真正的token后,前端就要在之后的接口中将这个token放入header中,至于这个header的字段名叫什么,在在这里插入图片描述
这里的配置文件中会配置,配置后,接口的登录校验就会从header中获取这个字段的值来进行登录校验

其他接口,调用的时候会校验是否登录

已经登录后的调用返回:
在这里插入图片描述
未登录的调用返回:
在这里插入图片描述

相关内容

热门资讯

伊朗在波斯湾查获一艘走私燃料的... 经济观察网 据央视新闻客户端消息,总台记者当地时间24日获悉,伊朗伊斯兰革命卫队海军在波斯湾的伊朗领...
科蒂进入转型关键期 转自:北京日报客户端科蒂迎来新的当家人。近日,科蒂官方宣布,Markus Strobel将于2026...
法官裁定特朗普政府10万美元H...   美国一名联邦法官作出裁定,特朗普政府可正式推行新申请 H-1B 签证需缴纳 10 万美元申请费的...
原创 认... 蒯曼今年先后与何卓佳、孙颖莎两个选手交手的过程当中,给进行发球的鹰眼挑战。特别是孙颖莎在总决赛的时候...
重疾险如何摆脱“买易赔难” 当医生说出“确诊”二字时,被保险人翻出那份保险合同,仿佛握住最后一根稻草。销售人员当年“确诊即赔”的...