SpringBoot实战:登录管理

发布于:2024-07-20 ⋅ 阅读:(160) ⋅ 点赞:(0)

认证方案

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

基于Sessioon

认证流程

特点:

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

基于ToKen 

特点

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

ToKen详解

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

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

 `header`(头部)

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

{
  "alg": "HS256",
  "typ": "JWT"
}

 `payload`(负载)

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

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

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

{
  "sub": "1234567890",
  "name": "John Doe",
  "iat": 1516239022
}

 `signature`(签名)

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

 

 登录流程

验证码接口 

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

导入相关依赖

开发接口

    @Operation(summary = "获取图形验证码")
    @GetMapping("login/captcha")
    public Result<CaptchaVo> getCaptcha() {
        CaptchaVo captchaVo = service.getCaptcha();
        return Result.ok(captchaVo);
    }
CaptchaVo getCaptcha();
    @Override
    public CaptchaVo getCaptcha() {

        SpecCaptcha specCaptcha = new SpecCaptcha(130, 48, 4);

        String code = specCaptcha.text().toLowerCase();
        String key = RedisConstant.ADMIN_LOGIN_PREFIX + UUID.randomUUID(); // key值需遵循命名规范

        stringRedisTemplate.opsForValue().set(key, code, RedisConstant.ADMIN_LOGIN_CAPTCHA_TTL_SEC, TimeUnit.SECONDS); // 60秒过期

        return new CaptchaVo(specCaptcha.toBase64(), key);
    }

登录接口

    @Operation(summary = "登录")
    @PostMapping("login")
    public Result<String> login(@RequestBody LoginVo loginVo) {
        String jwt = service.login(loginVo);
        return Result.ok(jwt);
    }
String login(LoginVo loginVo);
    @Override
    public String login(LoginVo loginVo) {

        // 前端发送`username`、`password`、`captchaKey`、`captchaCode`请求登录。

        // 判断`captchaCode`是否为空,若为空,则直接响应`验证码为空`;若不为空进行下一步判断。
        if (loginVo.getCaptchaCode() == null) {
            throw new LeaseException(ResultCodeEnum.ADMIN_CAPTCHA_CODE_NOT_FOUND);
        }

        // 根据`captchaKey`从Redis中查询之前保存的`code`,若查询出来的`code`为空,则直接响应`验证码已过期`;若不为空进行
        String code = stringRedisTemplate.opsForValue().get(loginVo.getCaptchaKey());
        if (code == null) {
            throw new LeaseException(ResultCodeEnum.ADMIN_CAPTCHA_CODE_EXPIRED);
        }

        // 判断`captchaCode`和之前保存的`code`是否相同,若不相同,则直接响应`验证码错误`;若相同进行下一步判断。
        if (!code.equals(loginVo.getCaptchaCode())) {
            throw new LeaseException(ResultCodeEnum.ADMIN_CAPTCHA_CODE_ERROR);
        }

        // 判断`username`和之前保存的`username`是否相同,若不相同,则直接响应`账号不存在`;若相同进行下一步判断。
        SystemUser systemUser = systemUserMapper.selectOneByUserName(loginVo.getUsername());
        if (systemUser == null) {
            throw new LeaseException(ResultCodeEnum.ADMIN_ACCOUNT_NOT_EXIST_ERROR);
        }

        // 查看用户状态,判断是否被禁用,若禁用,则直接响应`账号被禁`;若未被禁用,则进行下一步判断。
        if (systemUser.getStatus() == BaseStatus.DISABLE) {
            throw new LeaseException(ResultCodeEnum.ADMIN_ACCOUNT_DISABLED_ERROR);
        }

        // 判断`password`和之前保存的`password`是否相同,若不相同,则直接响应`账号或密码错误`;若相同进行下一步判断。
        if (!systemUser.getPassword().equals(DigestUtils.md5Hex(loginVo.getPassword()))) {
            throw new LeaseException(ResultCodeEnum.ADMIN_ACCOUNT_ERROR);
        }

        // 创建JWT,并响应给浏览器
        return JwtUtil.createToken(systemUser.getId(), systemUser.getUsername());
    }

JWT 

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

1.导入依赖

2.开发工具类

public class JwtUtil {

    private static SecretKey secretKey = Keys.hmacShaKeyFor("oUTzaoUGmKOzdHFx9eDSSZqtY32nugV6".getBytes()); //密钥

    // 创建token
    public static String createToken(Long userId, String username) {
        String jwt = Jwts.builder() //创建jwt工厂
                .setExpiration(new Date(System.currentTimeMillis() + 60 * 60 * 1000)) //设置过期时间
                .setSubject("Login_User") //设置主题
                .claim("userId", userId) //设置用户id"
                .claim("username", username) //设置用户名
                .signWith(secretKey, SignatureAlgorithm.HS256)//设置加密方式
                .compact();
        return jwt;
    }

    // 解析token
    public static Claims parseTaken(String token) {

        if (token == null) {
            throw new LeaseException(ResultCodeEnum.ADMIN_LOGIN_AUTH);
        }

        try {
            Jws<Claims> claimsJws = Jwts.parserBuilder()
                                        .setSigningKey(secretKey)
                                        .build()
                                        .parseClaimsJws(token);
            return claimsJws.getBody();
        } catch (ExpiredJwtException e) {
            throw new LeaseException(ResultCodeEnum.TOKEN_EXPIRED);
        } catch (JwtException e) {
            throw new LeaseException(ResultCodeEnum.TOKEN_INVALID);
        }
    }

}

配置拦截器

// 自定义拦截器
@Component
public class AuthenticationInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        String token = request.getHeader("access-token"); // 将jwt的key值设为access-token放入请求头中(该值需和前端商定一致)

        Claims claims = JwtUtil.parseTaken(token);//解析前端的token是否符合规则,符合即登录,可放行
        Long userId = claims.get("userId", Long.class);
        String username = claims.get("username", String.class);
        LoginUserHolder.setLoginUser(new LoginUser(userId, username)); // 放入登录用户信息,用于后续获取用户信息

        return true;
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        LoginUserHolder.clear(); // 清除登录用户信息
    }
}
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry
                .addInterceptor(authenticationInterceptor) // 拦截器
                .addPathPatterns("/admin/**") // 拦截所有/admin开头的请求
                .excludePathPatterns("/admin/login/**"); // 排除/admin/login开头的请求
    }
public class LoginUserHolder {
    public static ThreadLocal<LoginUser> threadLocal = new ThreadLocal<>();

    public static void setLoginUser(LoginUser loginUser) {
        threadLocal.set(loginUser);
    }

    public static LoginUser getLoginUser() {
        return threadLocal.get();
    }

    public static void clear() {
        threadLocal.remove();
    }
}