spring boot jwt 实现用户登录完整java

spring boot jwt 实现用户登录完整java

登录校验逻辑

用户登录的校验逻辑分为三个主要步骤,分别是校验验证码校验用户状态校验密码,具体逻辑如下

  • 前端发送usernamepasswordcaptchaKeycaptchaCode请求登录。
  • 判断captchaCode是否为空,若为空,则直接响应验证码为空;若不为空进行下一步判断。
  • 根据captchaKey从Redis中查询之前保存的code,若查询出来的code为空,则直接响应验证码已过期;若不为空进行下一步判断。
  • 比较captchaCodecode,若不相同,则直接响应验证码不正确;若相同则进行下一步判断。
  • 根据username查询数据库,若查询结果为空,则直接响应账号不存在;若不为空则进行下一步判断。
  • 查看用户状态,判断是否被禁用,若禁用,则直接响应账号被禁;若未被禁用,则进行下一步判断。
  • 比对password和数据库中查询的密码,若不一致,则直接响应账号或密码错误,若一致则进行入最后一步。
  • 创建JWT,并响应给浏览器。

请求数据结构

java 复制代码
package com.orchids.springmybatisplus.model.entity;

import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;

/**
 * @Author qwh
 * @Date 2024/6/2 22:31
 */
@Data
@Schema(description = "后台管理系统登录信息")
public class LoginVo {

    @Schema(description="用户名")
    private String username;

    @Schema(description="密码")
    private String password;

    @Schema(description="验证码key")
    private String captchaKey;

    @Schema(description="验证码code")
    private String captchaCode;
}

枚举类

java 复制代码
package com.orchids.lovehouse.common.result;

import lombok.Getter;

/**
 * 统一返回结果状态信息类
 */
@Getter
public enum ResultCodeEnum {

    SUCCESS(200, "成功"),
    FAIL(201, "失败"),
    PARAM_ERROR(202, "参数不正确"),
    SERVICE_ERROR(203, "服务异常"),
    DATA_ERROR(204, "数据异常"),
    ILLEGAL_REQUEST(205, "非法请求"),
    REPEAT_SUBMIT(206, "重复提交"),
    DELETE_ERROR(207, "请先删除子集"),

    ADMIN_ACCOUNT_EXIST_ERROR(301, "账号已存在"),
    ADMIN_CAPTCHA_CODE_ERROR(302, "验证码错误"),
    ADMIN_CAPTCHA_CODE_EXPIRED(303, "验证码已过期"),
    ADMIN_CAPTCHA_CODE_NOT_FOUND(304, "未输入验证码"),
    ADMIN_ACCOUNT_NOT_EXIST(330,"用户不存在"),


    ADMIN_LOGIN_AUTH(305, "未登陆"),
    ADMIN_ACCOUNT_NOT_EXIST_ERROR(306, "账号不存在"),
    ADMIN_ACCOUNT_ERROR(307, "用户名或密码错误"),
    ADMIN_ACCOUNT_DISABLED_ERROR(308, "该用户已被禁用"),
    ADMIN_ACCESS_FORBIDDEN(309, "无访问权限"),
    APP_LOGIN_AUTH(501, "未登陆"),
    APP_LOGIN_PHONE_EMPTY(502, "手机号码为空"),
    APP_LOGIN_CODE_EMPTY(503, "验证码为空"),
    APP_SEND_SMS_TOO_OFTEN(504, "验证法发送过于频繁"),
    APP_LOGIN_CODE_EXPIRED(505, "验证码已过期"),
    APP_LOGIN_CODE_ERROR(506, "验证码错误"),
    APP_ACCOUNT_DISABLED_ERROR(507, "该用户已被禁用"),


    TOKEN_EXPIRED(601, "token过期"),
    TOKEN_INVALID(602, "token非法");


    private final Integer code;

    private final String message;

    ResultCodeEnum(Integer code, String message) {
        this.code = code;
        this.message = message;
    }
}

全局异常处理

java 复制代码
package com.orchids.lovehouse.common.exception;

import com.orchids.lovehouse.common.result.ResultCodeEnum;
import lombok.Data;

/**
 * @Author qwh
 * @Date 2024/6/1 20:18
 */
@Data
public class LovehouseException extends RuntimeException {

    //异常状态码
    private Integer code;
    /**
     * 通过状态码和错误消息创建异常对象
     * @param message
     * @param code
     */
    public LovehouseException(String message, Integer code) {
        super(message);
        this.code = code;
    }

    /**
     * 根据响应结果枚举对象创建异常对象
     * @param resultCodeEnum
     */
    public LovehouseException(ResultCodeEnum resultCodeEnum) {
        super(resultCodeEnum.getMessage());
        this.code = resultCodeEnum.getCode();
    }

    @Override
    public String toString() {
        return "LovehouseException{" +
                "code=" + code +
                ", message=" + this.getMessage() +
                '}';
    }
}

配置所需依赖

登录接口需要为登录成功的用户创建并返回JWT,本项目使用开源的JWT工具Java-JWT ,配置如下,具体内容可参考官方文档

  • 引入Maven依赖
xml 复制代码
<!-- https://mvnrepository.com/artifact/io.jsonwebtoken/jjwt-api -->
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-api</artifactId>
    <version>0.11.2</version>
</dependency>


<dependency><!-- https://mvnrepository.com/artifact/io.jsonwebtoken/jjwt-impl -->
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-impl</artifactId>
    <version>0.11.2</version>
    <scope>runtime</scope>
</dependency>
  <!-- https://mvnrepository.com/artifact/io.jsonwebtoken/jjwt-jackson -->
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-jackson</artifactId>
    <version>0.11.2</version>
    <scope>runtime</scope>
</dependency>

创建JWT和工具类 common.utils.JwtUtil

java 复制代码
package com.orchids.lovehouse.common.utils;

import com.orchids.lovehouse.common.exception.LovehouseException;
import com.orchids.lovehouse.common.result.ResultCodeEnum;
import io.jsonwebtoken.*;
import io.jsonwebtoken.security.Keys;
import io.jsonwebtoken.security.SignatureException;

import javax.crypto.SecretKey;
import java.util.Date;

/**
 * @Author qwh
 * @Date 2024/6/2 21:01
 */
public class JwtUtil {
    private static long tokenExpiration = 60  * 60 * 1000L;
    public static SecretKey secretKey = Keys.hmacShaKeyFor("M0PKKI6pYGVWWfDZw90a0lTpGYX1d4AQ".getBytes());
    public static String createToken(Long userId,String username){
        String token  = Jwts.builder().
        setSubject("USER_INFO").
        setExpiration(new Date(System.currentTimeMillis()+tokenExpiration)).
        claim("userId",userId).
        claim("username",username).
        signWith(secretKey,SignatureAlgorithm.HS256).
        compact();
        return token;
    }
    public static Claims parsToken(String token){
        if (token==null) {
            throw new LovehouseException(ResultCodeEnum.ADMIN_LOGIN_AUTH);
        }
        try {
            JwtParser jwtParser = Jwts.parserBuilder().setSigningKey(secretKey).build();
            Jws<Claims> claims = jwtParser.parseClaimsJws(token);
            return claims.getBody();
        } catch (ExpiredJwtException e) {
            throw new LovehouseException(ResultCodeEnum.TOKEN_EXPIRED);
        } catch (JwtException e){
            throw new LovehouseException(ResultCodeEnum.TOKEN_INVALID);
        }
    }

    public static void main(String[] args) {
        System.out.println(createToken(2l,"user"));
    }
}

controller逻辑

java 复制代码
package com.orchids.lovehouse.web.admin.controller.login;


import com.orchids.lovehouse.common.login.LoginUserHolder;
import com.orchids.lovehouse.common.result.Result;
import com.orchids.lovehouse.common.utils.JwtUtil;
import com.orchids.lovehouse.web.admin.service.LoginService;
import com.orchids.lovehouse.web.admin.vo.login.CaptchaVo;
import com.orchids.lovehouse.web.admin.vo.login.LoginVo;
import com.orchids.lovehouse.web.admin.vo.system.user.SystemUserInfoVo;
import com.orchids.lovehouse.web.admin.vo.system.user.SystemUserItemVo;
import io.jsonwebtoken.Claims;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

@Tag(name = "后台管理系统登录管理")
@RestController
@RequestMapping("/admin")
public class LoginController {
    @Autowired
    private LoginService loginService;

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

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

    @Operation(summary = "获取登陆用户个人信息")
    @GetMapping("info")
    public Result<SystemUserInfoVo> info () {
        SystemUserInfoVo systemUserInfo  = loginService.getLoginUserInfo();
        return Result.ok(systemUserInfo);
    }
}

service逻辑

java 复制代码
package com.orchids.lovehouse.web.admin.service;

import com.orchids.lovehouse.web.admin.vo.login.CaptchaVo;
import com.orchids.lovehouse.web.admin.vo.login.LoginVo;
import com.orchids.lovehouse.web.admin.vo.system.user.SystemUserInfoVo;

public interface LoginService {

    CaptchaVo getCaptcha();

    String login(LoginVo loginVo);

    SystemUserInfoVo getLoginUserInfo(Long userId);
}

sreviceImpl

java 复制代码
package com.orchids.lovehouse.web.admin.service.impl;

import com.orchids.lovehouse.common.constant.RedisConstant;
import com.orchids.lovehouse.common.exception.GlobalExceptionHandler;
import com.orchids.lovehouse.common.exception.LovehouseException;
import com.orchids.lovehouse.common.result.ResultCodeEnum;
import com.orchids.lovehouse.common.utils.JwtUtil;
import com.orchids.lovehouse.model.entity.SystemUser;
import com.orchids.lovehouse.model.enums.BaseStatus;
import com.orchids.lovehouse.web.admin.mapper.SystemUserMapper;
import com.orchids.lovehouse.web.admin.service.LoginService;
import com.orchids.lovehouse.web.admin.vo.login.CaptchaVo;
import com.orchids.lovehouse.web.admin.vo.login.LoginVo;
import com.orchids.lovehouse.web.admin.vo.system.user.SystemUserInfoVo;
import com.orchids.lovehouse.web.admin.vo.system.user.SystemUserItemVo;
import com.wf.captcha.SpecCaptcha;
import com.wf.captcha.base.Captcha;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.util.DigestUtils;
import org.springframework.util.StringUtils;

import java.util.UUID;
import java.util.concurrent.TimeUnit;

@Service
public class LoginServiceImpl implements LoginService {
    @Autowired
    private StringRedisTemplate stringRedisTemplate;
    @Autowired
    private SystemUserMapper systemUserMapper;

    @Override
    public CaptchaVo getCaptcha() {
        SpecCaptcha  specCaptcha = new SpecCaptcha(100, 40, 5);
        specCaptcha.setCharType(Captcha.TYPE_DEFAULT);

        String code = specCaptcha.text().toLowerCase();
        String key = RedisConstant.ADMIN_LOGIN_PREFIX + UUID.randomUUID();

        String img = specCaptcha.toBase64();
        stringRedisTemplate.opsForValue().set(key,code,60, TimeUnit.SECONDS);
        return new CaptchaVo(img,key);
    }

    @Override
    public String login(LoginVo loginVo) {
        //判断是否输入验证码
        if (!StringUtils.hasText(loginVo.getCaptchaCode())) {
            throw new LovehouseException(ResultCodeEnum.ADMIN_CAPTCHA_CODE_NOT_FOUND);
        }
        //校验验证码
        String code = stringRedisTemplate.opsForValue().get(loginVo.getCaptchaKey());
        if (code == null){
            throw new LovehouseException(ResultCodeEnum.APP_LOGIN_CODE_EXPIRED);
        }
        if (!code.equals(loginVo.getCaptchaCode())){
            throw new LovehouseException(ResultCodeEnum.APP_LOGIN_CODE_ERROR);
        }
        //校验用户是否存在
        SystemUser systemUser = systemUserMapper.selectOneByUsername(loginVo.getUsername());
        if (systemUser == null) {
            throw new LovehouseException(ResultCodeEnum.ADMIN_ACCOUNT_NOT_EXIST);
        }
        if (systemUser.getStatus() == BaseStatus.DISABLE) {
            throw new LovehouseException(ResultCodeEnum.ADMIN_ACCOUNT_DISABLED_ERROR);
        }
        // 鏍¢獙鐢ㄦ埛瀵嗙爜
        if (!systemUser.getPassword().equals(DigestUtils.md5DigestAsHex(loginVo.getPassword().getBytes()))) {
            throw new LovehouseException(ResultCodeEnum.ADMIN_ACCOUNT_ERROR);
        }
        // 鍒涘缓骞惰繑鍥瀟oken
        return JwtUtil.createToken(systemUser.getId(),systemUser.getUsername());
    }

    @Override
    public SystemUserInfoVo getLoginUserInfo(Long userId) {
        SystemUser systemUser = systemUserMapper.selectById(userId);
        SystemUserInfoVo systemUserInfoVo = new SystemUserInfoVo();
        systemUserInfoVo.setName(systemUser.getName());
        systemUserInfoVo.setAvatarUrl(systemUser.getAvatarUrl());
        return systemUserInfoVo;
    }


}

编写mapper逻辑

java 复制代码
SystemUser selectOneByUsername(String username);

mapper.xml

java 复制代码
写入对应的sql到xml文件

编写HandlerInterceptor

保护所有受保护的接口增加jwt合法性逻辑 custom.interceptor.AuthenticationInterceptor

java 复制代码
package com.orchids.lovehouse.web.admin.custom.interceptor;

import com.orchids.lovehouse.common.login.LoginUser;
import com.orchids.lovehouse.common.login.LoginUserHolder;
import io.jsonwebtoken.Claims;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import com.orchids.lovehouse.common.utils.JwtUtil;

/**
 * @Author qwh
 * @Date 2024/6/2 21:55
 */
@Component
public class AuthenticationInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        String token = request.getHeader("access-token");

        Claims claims = JwtUtil.parsToken(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();
    }
}

我们约定,前端登录后,后续请求都将JWT,放置于HTTP请求的Header中,其Header的key为access-token

注册HanderInterceptor config.WebMvcConfiguration

java 复制代码
package com.orchids.lovehouse.web.admin.custom.config;

import com.orchids.lovehouse.web.admin.custom.converter.StringToBaseEnumConverterFactory;
import com.orchids.lovehouse.web.admin.custom.converter.StringToItemTypeConverter;
import com.orchids.lovehouse.web.admin.custom.interceptor.AuthenticationInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.format.FormatterRegistry;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class WebMvcConfiguration implements WebMvcConfigurer {
    
    @Autowired
    private AuthenticationInterceptor authenticationInterceptor;
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(this.authenticationInterceptor).addPathPatterns("/admin/**").excludePathPatterns("/admin/login/**");
    }
}

获取登录个人信息
查看请求和响应的数据结构

  • 响应的数据结构
java 复制代码
@Schema(description = "员工基本信息")
@Data
public class SystemUserInfoVo {

    @Schema(description = "用户姓名")
    private String name;

    @Schema(description = "用户头像")
    private String avatarUrl;
}
相关推荐
Rust研习社12 小时前
组合真的优于继承吗?为什么 Rust 和 Go 都拥抱组合舍弃继承?
后端·rust·编程语言
IT_陈寒13 小时前
JavaScript的闭包把我坑惨了,说好的内存会自动回收呢?
前端·人工智能·后端
CaffeinePro13 小时前
Pydantic深度使用:数据校验、枚举、ORM映射
后端·fastapi
Chenyiax14 小时前
从 Chat 到 Responses:OpenAI API 抽象为什么变了?
后端
MariaH14 小时前
Koa和Express的区别
后端
MariaH14 小时前
Koa框架的使用
后端
luckdewei15 小时前
那个用 passlib 做认证的新同事,上线第一天就把用户密码写进了日志
后端
ping某16 小时前
为什么 Nginx 明明监听了 80,转发后端时却用了 4xxxx 端口?
后端·nginx
JustHappy17 小时前
我汇总了身边朋友的经历才发现,其实第一份实习是最难找的......
前端·后端·面试
uhakadotcom17 小时前
在python 的 工程化架构中 ,什么是 薄包装器层?
后端·面试·github