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

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

如图所示:

相关推荐
江深竹静,一苇以航1 分钟前
springboot3项目整合Mybatis-plus启动项目报错:Invalid bean definition with name ‘xxxMapper‘
java·spring boot
杜杜的man32 分钟前
【go从零单排】go中的结构体struct和method
开发语言·后端·golang
幼儿园老大*33 分钟前
走进 Go 语言基础语法
开发语言·后端·学习·golang·go
llllinuuu35 分钟前
Go语言结构体、方法与接口
开发语言·后端·golang
Karoku06635 分钟前
【企业级分布式系统】Zabbix监控系统与部署安装
运维·服务器·数据库·redis·mysql·zabbix
cookies_s_s35 分钟前
Golang--协程和管道
开发语言·后端·golang
为什么这亚子38 分钟前
九、Go语言快速入门之map
运维·开发语言·后端·算法·云原生·golang·云计算
豪宇刘43 分钟前
SpringBoot+Shiro权限管理
java·spring boot·spring
gorgor在码农1 小时前
Redis 热key总结
java·redis·热key
想进大厂的小王1 小时前
项目架构介绍以及Spring cloud、redis、mq 等组件的基本认识
redis·分布式·后端·spring cloud·微服务·架构