spring boot jwt 实现用户登录完整java
登录校验逻辑
用户登录的校验逻辑分为三个主要步骤,分别是校验验证码 ,校验用户状态 和校验密码,具体逻辑如下
- 前端发送
username
、password
、captchaKey
、captchaCode
请求登录。 - 判断
captchaCode
是否为空,若为空,则直接响应验证码为空
;若不为空进行下一步判断。 - 根据
captchaKey
从Redis中查询之前保存的code
,若查询出来的code
为空,则直接响应验证码已过期
;若不为空进行下一步判断。 - 比较
captchaCode
和code
,若不相同,则直接响应验证码不正确
;若相同则进行下一步判断。 - 根据
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;
}