登录流程:
1、前端调用登录接口,往接口里传入账号,密码
2、根据账号判断是否有这个用户,如果有则继续判断密码是否正确
3、验证成功后,则是根据账号,登录时间生成token(用JWT)
4、将token存入Redis当中,用于token过期策略
5、将token和用户信息返回给前端
6、此后调用后端任何接口都会先判断发来请求里token是否存在、有效(拦截器实现)
7、然后继续接下来的正常调用
具体思路:
1、再登录接口中实现账号、密码的验证和token创建
2、实现一个拦截器,拦截除登录接口外的其他所有接口
3、再拦截器中从请求头中取出token对其进行正确性的验证
4、然后从redis<account,token>里取出属于这个账号的token,如果取的出来则说明这个用户登录状态没有过期,然后和取出来的token进行对比,如果不一样则说明同一账号再其他地方进行了登录,则被挤出登录状态。
5、再这整个判断过程中所产生的异常(没有登录状态,不存在这个用户....)都是由全局异常处理器进行捕获然后返回给前端。
pom文件要引的依赖:
<!--版本-->
<properties>
<java.version>1.8</java.version>
<lombok.version>1.18.12</lombok.version>
<hutool.version>5.8.8</hutool.version>
<mybatis-plus.version>3.5.2</mybatis-plus.version>
<JWT.version>6.0</JWT.version>
</properties>
<!--JWT-->
<dependency>
<groupId>com.nimbusds</groupId>
<artifactId>nimbus-jose-jwt</artifactId>
<version>${JWT.version}</version>
</dependency>
<dependency>
<groupId>com.qcby</groupId>
<artifactId>qcby-common</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<!--redis-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
</dependency>
<!--hutool-->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>${hutool.version}</version>
</dependency>
对于token我使用了JWT,有一个token的工具类用于token的创建和 验证。(这个类里使用的redisUtil类大家可以从网上随便找一个redis工具类,绑定上自己的reids)
import com.nimbusds.jose.*;
import com.nimbusds.jose.crypto.MACSigner;
import com.nimbusds.jose.crypto.MACVerifier;
import com.nimbusds.jwt.JWTClaimsSet;
import com.nimbusds.jwt.SignedJWT;
import com.qcby.framework.common.exception.ServiceException;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.text.ParseException;
import java.util.Date;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
@Component
public class TokenUtil {
@Resource
RedisUtil redisUtil;
/**
* 创建秘钥
*/
private static final byte[] SECRET = "qngChengBoYa-realtimeWuIngWangJiaQiZhangYv".getBytes();
/**
* 生成token
* @param account
* @return {@link String}
*/
public String buildToken(String account) {
try {
/**
* 1.创建一个32-byte的密匙
*/
MACSigner macSigner = new MACSigner(SECRET);
/**
* 2. 建立payload 载体
*/
JWTClaimsSet claimsSet = new JWTClaimsSet.Builder()
.subject("login")
.claim("ACCOUNT",account)
.issueTime(new Date())
.build();
/**
* 3. 建立签名
*/
SignedJWT signedJWT = new SignedJWT(new JWSHeader(JWSAlgorithm.HS256), claimsSet);
signedJWT.sign(macSigner);
/**
* 4. 生成token
*/
String token = signedJWT.serialize();
redisUtil.setEx(account,token,10,TimeUnit.MINUTES);
return token;
} catch (KeyLengthException e) {
e.printStackTrace();
} catch (JOSEException e) {
e.printStackTrace();
}
return null;
}
/**
* 校验token
* @param token
* @return
*/
public boolean verifyToken(String token) {
try {
SignedJWT jwt = SignedJWT.parse(token);
JWSVerifier verifier = new MACVerifier(SECRET);
/**
* 校验是否有效
*/
if (!jwt.verify(verifier)) {
return false;
}
/**
* 获取载体中的数据
*/
String account = (String) jwt.getJWTClaimsSet().getClaim("ACCOUNT");
//是否有
if (Objects.isNull(account)){
return false;
}
/**
* 判断redis里是否有account为key的值,如果有
* 判断token是否和redis里存的是是否一样,
* 如果不一样说明已经有其他账号登录了,则回到登录页面
* 如果一样,则给token续期
*/
if (redisUtil.hasKey(account)){
String s = redisUtil.get(account);
if (s.equals(token)){
redisUtil.expire(account,10,TimeUnit.MINUTES);
return true;
}
throw new ServiceException("422","有其他设备登录");
}
} catch (ParseException e) {
e.printStackTrace();
} catch (JOSEException e) {
e.printStackTrace();
}
return false;
}
}
登录拦截器的实现,要先创建一个类并实现HandlerInterceptor这个接口,然后再创建一个拦截器的配置类令其实现WebmvcConfigurer这个接口,重新addInterceptors方法,再这个方法中将之前实现的登录拦截器给注册进去,并配置这个拦截器的拦截路径,拦截优先级等等。
/**
* 请求拦截器
* @author MI
* @date 2023/10/03
*/
@Component
public class LoginInterceptor implements HandlerInterceptor {
private static Logger log = Logger.getLogger(LoginInterceptor.class);
/***
* 在请求处理之前进行调用(Controller方法调用之前)
@param request
@param response
@param handler
@return boolean
@throws Exception
*/
@Resource
TokenUtil tokenUtil;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
/**
* 从请求头中取出token,并判断其是否存在和合法
*/
String token = request.getHeader("token");
if (token != null && tokenUtil.verifyToken(token)) {
return true;
}else {
throw new ServiceException("100","还未登录");
}
}
/***
* 请求处理之后进行调用,但是在视图被渲染之前(Controller方法调用之后)
@param request
@param response
@param handler
@param modelAndView
@throws Exception
*/
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
}
/***
* 整个请求结束之后被调用,也就是在DispatchServlet渲染了对应的视图之后执行(主要用于进行资源清理工作)
@param request
@param response
@param handler
@param ex
@throws Exception
*/
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
}
}
import javax.annotation.Resource;
@Configuration
public class MyWebMvcConfig implements WebMvcConfigurer {
@Resource
LoginInterceptor loginInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
/**
* 登录拦截器
* */
registry.addInterceptor(loginInterceptor)
.addPathPatterns("/**")
.excludePathPatterns("/login").
order(1);
}
}
全局异常处理器,它能捕获到全部的异常(前提是要把异常抛出),所有异常都会在controller层反应出来,因为执行方法的原头在controller。我之前就是用try-catch处理,然后一直一直捕获不到,然后网上说正常的实现的全局异常器只能捕获到controller层的异常,所以拦截器里的异常捕获不到,这句话对也不对。拦截器里异常确实捕获不到,但只要咱把它抛出去就能再controller层显现了。
具体实现就是我们要加@ControllerAdvice注解, @ExceptionHandler根据这个注解具体绑定处理哪个异常。
/**
* 全局异常处理器
* @author MI
* @date 2023/10/02
*/
@Slf4j
@ControllerAdvice
public class GlobalExceptionHandler{
/**
* 自定义异常拦截器
* @param req
* @param e
* @return {@link Result}
*/
@ResponseBody
@ExceptionHandler(value =ServiceException.class)
public Result exceptionHandler(HttpServletRequest req, ServiceException e){
log.info("发送{}异常",e.getMessage());
return Result.getBusinessException(e.getLocalizedMessage(),e.getCode());
}
@ResponseBody
@ExceptionHandler(value =Exception.class)
public Result exceptionHandler(HttpServletRequest req, Exception e){
log.info("发送{}异常",e.getMessage());
return Result.getBusinessException(e.getLocalizedMessage());
}
}
Server层实现:
@Service
@Slf4j
public class LoginServiceImpl implements ILoginService {
@Resource
UserMapper userMapper;
@Resource
TokenUtil tokenUtil;
@Resource
UserRoleMapper userRoleMapper;
/**
* 登录实现
* @param loginDto
* @return {@link LoginVo}
*/
@Override
public LoginVo login(LoginDto loginDto) {
UserPo userPo = userMapper.selectOne(new LambdaQueryWrapper<UserPo>().eq(UserPo::getAccount, loginDto.getAccount()));
if (userPo!=null){
if (userPo.getPassword().equals(loginDto.getPassword())){
/**
* 构建token
*/
String token = tokenUtil.buildToken(userPo.getAccount());
LoginVo loginVo = new LoginVo();
loginVo.setToken(token);
loginVo.setRoleId(userRoleMapper.selectOne(
new LambdaQueryWrapper<UserRolePo>().eq(UserRolePo::getUserId,userPo.getUserId())).getRoleId());
loginVo.setAccount(userPo.getAccount());
return loginVo;
}else{
throw new ServiceException("422","密码错误");
}
}else {
throw new ServiceException("422", "用户不存在");
}
}
}
后面的Controller、Mapper、实体层大家要根据自己的需求字段来进行具体实现,我就不贴出来了。
具体用法,像这个异常咱直接抛出来就行,全局异常处理类都能捕获,就不会继续往下执行了,它会把报错信息返回给前端:
如图所示: