Springboot实现登录功能(token、redis、登录拦截器、全局异常处理)

登录流程:

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、实体层大家要根据自己的需求字段来进行具体实现,我就不贴出来了。

具体用法,像这个异常咱直接抛出来就行,全局异常处理类都能捕获,就不会继续往下执行了,它会把报错信息返回给前端:

如图所示:

相关推荐
金銀銅鐵25 分钟前
[Java] 用 GUI 展示 EnumSet 增减元素时的内部变化(上)
java·后端·ai编程
念何架构之路1 小时前
go-grpc设计思想
开发语言·后端·golang
Web极客码1 小时前
我用 Claude 分析 Redis 哨兵日志,实现跨机房数据不一致自动对齐
数据库·redis·php
system_error_x1 小时前
SpringBoot 集成 Thymeleaf 简洁入门
java·spring boot·后端
步行cgn3 小时前
Spring MVC 视图机制的核心接口与类
java·开发语言·后端·spring·mvc
码事漫谈4 小时前
AI 读你的提示词,其实很没耐心
后端
用户8356290780515 小时前
如何使用 Python 格式化 Excel 单元格中的长文本
后端·python
我命由我123455 小时前
Python 开发 - Python 的 pathlib
java·开发语言·后端·python·学习·java-ee·学习方法
IT_陈寒5 小时前
Java性能优化完全指南
前端·人工智能·后端
电子IT笔记5 小时前
对接群晖NAS,上传和删除踩坑记录
前端·后端