• SpringBoot实战:登录管理


    认证方案

    有两种常见的认证方案,分别是基于Session的认证和基于Token的认证

    基于Sessioon

    认证流程

    特点:

    * 登录用户信息保存在服务端内存中,若访问量增加,单台节点压力会较大
    * 随用户规模增大,若后台升级为集群,则需要解决集群中各服务器登录状态共享的问题。 

    基于ToKen 

    特点

    * 登录状态保存在客户端,服务器没有存储开销
    * 客户端发起的每个请求自身均携带登录状态,所以即使后台为集群,也不会面临登录状态共享的问题。 

    ToKen详解

     我们所说的Token,通常指JWT(JSON Web TOKEN)。JWT是一种轻量级的安全传输方式,用于在两个实体之间传递信息,通常用于身份验证和信息传递。

    JWT是一个字符串,如下图所示,该字符串由三部分组成,三部分由`.`分隔。三个部分分别被称为

     `header`(头部)

    Header部分是由一个JSON对象经过`base64url`编码得到的,这个JSON对象用于保存JWT 的类型(`typ`)、签名算法(`alg`)等元信息,例如

    1. {
    2. "alg": "HS256",
    3. "typ": "JWT"
    4. }

     `payload`(负载)

    也称为 Claims(声明),也是由一个JSON对象经过`base64url`编码得到的,用于保存要传递的具体信息。JWT规范定义了7个官方字段,如下:

    * iss (issuer):签发人
    * exp (expiration time):过期时间
    * sub (subject):主题
    * aud (audience):受众
    * nbf (Not Before):生效时间
    * iat (Issued At):签发时间
    * jti (JWT ID):编号

    除此之外,我们还可以自定义任何字段,例如

    1. {
    2. "sub": "1234567890",
    3. "name": "John Doe",
    4. "iat": 1516239022
    5. }

     `signature`(签名)

    由头部、负载和秘钥一起经过(header中指定的签名算法)计算得到的一个字符串,用于防止消息被篡改。

     

     登录流程

    验证码接口 

    本项目使用开源的验证码生成工具**EasyCaptcha**,其支持多种类型的验证码,例如gif、中文、算术等,并且简单易用,具体内容可参考其[官方文档](https://gitee.com/ele-admin/EasyCaptcha)。 

    导入相关依赖

    开发接口

    1. @Operation(summary = "获取图形验证码")
    2. @GetMapping("login/captcha")
    3. public Result getCaptcha() {
    4. CaptchaVo captchaVo = service.getCaptcha();
    5. return Result.ok(captchaVo);
    6. }
    CaptchaVo getCaptcha();
    1. @Override
    2. public CaptchaVo getCaptcha() {
    3. SpecCaptcha specCaptcha = new SpecCaptcha(130, 48, 4);
    4. String code = specCaptcha.text().toLowerCase();
    5. String key = RedisConstant.ADMIN_LOGIN_PREFIX + UUID.randomUUID(); // key值需遵循命名规范
    6. stringRedisTemplate.opsForValue().set(key, code, RedisConstant.ADMIN_LOGIN_CAPTCHA_TTL_SEC, TimeUnit.SECONDS); // 60秒过期
    7. return new CaptchaVo(specCaptcha.toBase64(), key);
    8. }

    登录接口

    1. @Operation(summary = "登录")
    2. @PostMapping("login")
    3. public Result login(@RequestBody LoginVo loginVo) {
    4. String jwt = service.login(loginVo);
    5. return Result.ok(jwt);
    6. }
    String login(LoginVo loginVo);
    1. @Override
    2. public String login(LoginVo loginVo) {
    3. // 前端发送`username`、`password`、`captchaKey`、`captchaCode`请求登录。
    4. // 判断`captchaCode`是否为空,若为空,则直接响应`验证码为空`;若不为空进行下一步判断。
    5. if (loginVo.getCaptchaCode() == null) {
    6. throw new LeaseException(ResultCodeEnum.ADMIN_CAPTCHA_CODE_NOT_FOUND);
    7. }
    8. // 根据`captchaKey`从Redis中查询之前保存的`code`,若查询出来的`code`为空,则直接响应`验证码已过期`;若不为空进行
    9. String code = stringRedisTemplate.opsForValue().get(loginVo.getCaptchaKey());
    10. if (code == null) {
    11. throw new LeaseException(ResultCodeEnum.ADMIN_CAPTCHA_CODE_EXPIRED);
    12. }
    13. // 判断`captchaCode`和之前保存的`code`是否相同,若不相同,则直接响应`验证码错误`;若相同进行下一步判断。
    14. if (!code.equals(loginVo.getCaptchaCode())) {
    15. throw new LeaseException(ResultCodeEnum.ADMIN_CAPTCHA_CODE_ERROR);
    16. }
    17. // 判断`username`和之前保存的`username`是否相同,若不相同,则直接响应`账号不存在`;若相同进行下一步判断。
    18. SystemUser systemUser = systemUserMapper.selectOneByUserName(loginVo.getUsername());
    19. if (systemUser == null) {
    20. throw new LeaseException(ResultCodeEnum.ADMIN_ACCOUNT_NOT_EXIST_ERROR);
    21. }
    22. // 查看用户状态,判断是否被禁用,若禁用,则直接响应`账号被禁`;若未被禁用,则进行下一步判断。
    23. if (systemUser.getStatus() == BaseStatus.DISABLE) {
    24. throw new LeaseException(ResultCodeEnum.ADMIN_ACCOUNT_DISABLED_ERROR);
    25. }
    26. // 判断`password`和之前保存的`password`是否相同,若不相同,则直接响应`账号或密码错误`;若相同进行下一步判断。
    27. if (!systemUser.getPassword().equals(DigestUtils.md5Hex(loginVo.getPassword()))) {
    28. throw new LeaseException(ResultCodeEnum.ADMIN_ACCOUNT_ERROR);
    29. }
    30. // 创建JWT,并响应给浏览器
    31. return JwtUtil.createToken(systemUser.getId(), systemUser.getUsername());
    32. }

    JWT 

    登录接口需要为登录成功的用户创建并返回JWT,本项目使用开源的JWT工具**Java-JWT**,配置如下,具体内容可参考[官方文档](https://github.com/jwtk/jjwt/tree/0.11.2)。

    1.导入依赖

    2.开发工具类

    1. public class JwtUtil {
    2. private static SecretKey secretKey = Keys.hmacShaKeyFor("oUTzaoUGmKOzdHFx9eDSSZqtY32nugV6".getBytes()); //密钥
    3. // 创建token
    4. public static String createToken(Long userId, String username) {
    5. String jwt = Jwts.builder() //创建jwt工厂
    6. .setExpiration(new Date(System.currentTimeMillis() + 60 * 60 * 1000)) //设置过期时间
    7. .setSubject("Login_User") //设置主题
    8. .claim("userId", userId) //设置用户id"
    9. .claim("username", username) //设置用户名
    10. .signWith(secretKey, SignatureAlgorithm.HS256)//设置加密方式
    11. .compact();
    12. return jwt;
    13. }
    14. // 解析token
    15. public static Claims parseTaken(String token) {
    16. if (token == null) {
    17. throw new LeaseException(ResultCodeEnum.ADMIN_LOGIN_AUTH);
    18. }
    19. try {
    20. Jws claimsJws = Jwts.parserBuilder()
    21. .setSigningKey(secretKey)
    22. .build()
    23. .parseClaimsJws(token);
    24. return claimsJws.getBody();
    25. } catch (ExpiredJwtException e) {
    26. throw new LeaseException(ResultCodeEnum.TOKEN_EXPIRED);
    27. } catch (JwtException e) {
    28. throw new LeaseException(ResultCodeEnum.TOKEN_INVALID);
    29. }
    30. }
    31. }

    配置拦截器

    1. // 自定义拦截器
    2. @Component
    3. public class AuthenticationInterceptor implements HandlerInterceptor {
    4. @Override
    5. public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
    6. String token = request.getHeader("access-token"); // 将jwt的key值设为access-token放入请求头中(该值需和前端商定一致)
    7. Claims claims = JwtUtil.parseTaken(token);//解析前端的token是否符合规则,符合即登录,可放行
    8. Long userId = claims.get("userId", Long.class);
    9. String username = claims.get("username", String.class);
    10. LoginUserHolder.setLoginUser(new LoginUser(userId, username)); // 放入登录用户信息,用于后续获取用户信息
    11. return true;
    12. }
    13. @Override
    14. public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
    15. LoginUserHolder.clear(); // 清除登录用户信息
    16. }
    17. }
    1. @Override
    2. public void addInterceptors(InterceptorRegistry registry) {
    3. registry
    4. .addInterceptor(authenticationInterceptor) // 拦截器
    5. .addPathPatterns("/admin/**") // 拦截所有/admin开头的请求
    6. .excludePathPatterns("/admin/login/**"); // 排除/admin/login开头的请求
    7. }
    1. public class LoginUserHolder {
    2. public static ThreadLocal threadLocal = new ThreadLocal<>();
    3. public static void setLoginUser(LoginUser loginUser) {
    4. threadLocal.set(loginUser);
    5. }
    6. public static LoginUser getLoginUser() {
    7. return threadLocal.get();
    8. }
    9. public static void clear() {
    10. threadLocal.remove();
    11. }
    12. }

  • 相关阅读:
    统计官方模型的参数量和计算量
    【Shell脚本12】Shell 输入/输出重定向
    下载和补全ComfyUI节点方法之一
    Java新手小白入门篇 Java面向对象(九)
    【C语言入门】ZZULIOJ 1000~1010
    C++(14):std::exchange
    微信小程序开发之后台数据交互及wxs应用
    SpringBoot SpringBoot 原理篇 1 自动配置 1.5 proxyBeanMethod
    【无标题】
    LuatOS-SOC接口文档(air780E)--keyboard - 键盘矩阵
  • 原文地址:https://blog.csdn.net/2301_79526467/article/details/140292866