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


    登录流程: 

            1、前端调用登录接口,往接口里传入账号,密码

            2、根据账号判断是否有这个用户,如果有则继续判断密码是否正确

            3、验证成功后,则是根据账号,登录时间生成token(用JWT

            4、将token存入Redis当中,用于token过期策略

            5、将token和用户信息返回给前端

            6、此后调用后端任何接口都会先判断发来请求里token是否存在、有效(拦截器实现)

            7、然后继续接下来的正常调用

     具体思路:

            1、再登录接口中实现账号、密码的验证和token创建

            2、实现一个拦截器,拦截除登录接口外的其他所有接口

            3、再拦截器中从请求头中取出token对其进行正确性的验证

            4、然后从redis里取出属于这个账号的token,如果取的出来则说明这个用户登录状态没有过期,然后和取出来的token进行对比,如果不一样则说明同一账号再其他地方进行了登录,则被挤出登录状态。

            5、再这整个判断过程中所产生的异常(没有登录状态,不存在这个用户....)都是由全局异常处理器进行捕获然后返回给前端。

     pom文件要引的依赖:

    1. <!--版本-->
    2. <properties>
    3. <java.version>1.8</java.version>
    4. <lombok.version>1.18.12</lombok.version>
    5. <hutool.version>5.8.8</hutool.version>
    6. <mybatis-plus.version>3.5.2</mybatis-plus.version>
    7. <JWT.version>6.0</JWT.version>
    8. </properties>
    9. <!--JWT-->
    10. <dependency>
    11. <groupId>com.nimbusds</groupId>
    12. <artifactId>nimbus-jose-jwt</artifactId>
    13. <version>${JWT.version}</version>
    14. </dependency>
    15. <dependency>
    16. <groupId>com.qcby</groupId>
    17. <artifactId>qcby-common</artifactId>
    18. <version>1.0-SNAPSHOT</version>
    19. </dependency>
    20. <!--redis-->
    21. <dependency>
    22. <groupId>org.springframework.boot</groupId>
    23. <artifactId>spring-boot-starter-data-redis</artifactId>
    24. </dependency>
    25. <dependency>
    26. <groupId>org.projectlombok</groupId>
    27. <artifactId>lombok</artifactId>
    28. <version>${lombok.version}</version>
    29. </dependency>
    30. <!--hutool-->
    31. <dependency>
    32. <groupId>cn.hutool</groupId>
    33. <artifactId>hutool-all</artifactId>
    34. <version>${hutool.version}</version>
    35. </dependency>

    对于token我使用了JWT,有一个token的工具类用于token的创建和 验证。(这个类里使用的redisUtil类大家可以从网上随便找一个redis工具类,绑定上自己的reids)

    1. import com.nimbusds.jose.*;
    2. import com.nimbusds.jose.crypto.MACSigner;
    3. import com.nimbusds.jose.crypto.MACVerifier;
    4. import com.nimbusds.jwt.JWTClaimsSet;
    5. import com.nimbusds.jwt.SignedJWT;
    6. import com.qcby.framework.common.exception.ServiceException;
    7. import org.springframework.stereotype.Component;
    8. import javax.annotation.Resource;
    9. import java.text.ParseException;
    10. import java.util.Date;
    11. import java.util.Objects;
    12. import java.util.concurrent.TimeUnit;
    13. @Component
    14. public class TokenUtil {
    15. @Resource
    16. RedisUtil redisUtil;
    17. /**
    18. * 创建秘钥
    19. */
    20. private static final byte[] SECRET = "qngChengBoYa-realtimeWuIngWangJiaQiZhangYv".getBytes();
    21. /**
    22. * 生成token
    23. * @param account
    24. * @return {@link String}
    25. */
    26. public String buildToken(String account) {
    27. try {
    28. /**
    29. * 1.创建一个32-byte的密匙
    30. */
    31. MACSigner macSigner = new MACSigner(SECRET);
    32. /**
    33. * 2. 建立payload 载体
    34. */
    35. JWTClaimsSet claimsSet = new JWTClaimsSet.Builder()
    36. .subject("login")
    37. .claim("ACCOUNT",account)
    38. .issueTime(new Date())
    39. .build();
    40. /**
    41. * 3. 建立签名
    42. */
    43. SignedJWT signedJWT = new SignedJWT(new JWSHeader(JWSAlgorithm.HS256), claimsSet);
    44. signedJWT.sign(macSigner);
    45. /**
    46. * 4. 生成token
    47. */
    48. String token = signedJWT.serialize();
    49. redisUtil.setEx(account,token,10,TimeUnit.MINUTES);
    50. return token;
    51. } catch (KeyLengthException e) {
    52. e.printStackTrace();
    53. } catch (JOSEException e) {
    54. e.printStackTrace();
    55. }
    56. return null;
    57. }
    58. /**
    59. * 校验token
    60. * @param token
    61. * @return
    62. */
    63. public boolean verifyToken(String token) {
    64. try {
    65. SignedJWT jwt = SignedJWT.parse(token);
    66. JWSVerifier verifier = new MACVerifier(SECRET);
    67. /**
    68. * 校验是否有效
    69. */
    70. if (!jwt.verify(verifier)) {
    71. return false;
    72. }
    73. /**
    74. * 获取载体中的数据
    75. */
    76. String account = (String) jwt.getJWTClaimsSet().getClaim("ACCOUNT");
    77. //是否有
    78. if (Objects.isNull(account)){
    79. return false;
    80. }
    81. /**
    82. * 判断redis里是否有account为key的值,如果有
    83. * 判断token是否和redis里存的是是否一样,
    84. * 如果不一样说明已经有其他账号登录了,则回到登录页面
    85. * 如果一样,则给token续期
    86. */
    87. if (redisUtil.hasKey(account)){
    88. String s = redisUtil.get(account);
    89. if (s.equals(token)){
    90. redisUtil.expire(account,10,TimeUnit.MINUTES);
    91. return true;
    92. }
    93. throw new ServiceException("422","有其他设备登录");
    94. }
    95. } catch (ParseException e) {
    96. e.printStackTrace();
    97. } catch (JOSEException e) {
    98. e.printStackTrace();
    99. }
    100. return false;
    101. }
    102. }

     登录拦截器的实现,要先创建一个类并实现HandlerInterceptor这个接口,然后再创建一个拦截器的配置类令其实现WebmvcConfigurer这个接口,重新addInterceptors方法,再这个方法中将之前实现的登录拦截器给注册进去,并配置这个拦截器的拦截路径,拦截优先级等等。

    1. /**
    2. * 请求拦截器
    3. * @author MI
    4. * @date 2023/10/03
    5. */
    6. @Component
    7. public class LoginInterceptor implements HandlerInterceptor {
    8. private static Logger log = Logger.getLogger(LoginInterceptor.class);
    9. /***
    10. * 在请求处理之前进行调用(Controller方法调用之前)
    11. @param request
    12. @param response
    13. @param handler
    14. @return boolean
    15. @throws Exception
    16. */
    17. @Resource
    18. TokenUtil tokenUtil;
    19. @Override
    20. public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
    21. /**
    22. * 从请求头中取出token,并判断其是否存在和合法
    23. */
    24. String token = request.getHeader("token");
    25. if (token != null && tokenUtil.verifyToken(token)) {
    26. return true;
    27. }else {
    28. throw new ServiceException("100","还未登录");
    29. }
    30. }
    31. /***
    32. * 请求处理之后进行调用,但是在视图被渲染之前(Controller方法调用之后)
    33. @param request
    34. @param response
    35. @param handler
    36. @param modelAndView
    37. @throws Exception
    38. */
    39. @Override
    40. public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
    41. }
    42. /***
    43. * 整个请求结束之后被调用,也就是在DispatchServlet渲染了对应的视图之后执行(主要用于进行资源清理工作)
    44. @param request
    45. @param response
    46. @param handler
    47. @param ex
    48. @throws Exception
    49. */
    50. @Override
    51. public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
    52. }
    53. }
    1. import javax.annotation.Resource;
    2. @Configuration
    3. public class MyWebMvcConfig implements WebMvcConfigurer {
    4. @Resource
    5. LoginInterceptor loginInterceptor;
    6. @Override
    7. public void addInterceptors(InterceptorRegistry registry) {
    8. /**
    9. * 登录拦截器
    10. * */
    11. registry.addInterceptor(loginInterceptor)
    12. .addPathPatterns("/**")
    13. .excludePathPatterns("/login").
    14. order(1);
    15. }
    16. }

            全局异常处理器,它能捕获到全部的异常(前提是要把异常抛出),所有异常都会在controller层反应出来,因为执行方法的原头在controller。我之前就是用try-catch处理,然后一直一直捕获不到,然后网上说正常的实现的全局异常器只能捕获到controller层的异常,所以拦截器里的异常捕获不到,这句话对也不对。拦截器里异常确实捕获不到,但只要咱把它抛出去就能再controller层显现了。

    具体实现就是我们要加@ControllerAdvice注解, @ExceptionHandler根据这个注解具体绑定处理哪个异常。

    1. /**
    2. * 全局异常处理器
    3. * @author MI
    4. * @date 2023/10/02
    5. */
    6. @Slf4j
    7. @ControllerAdvice
    8. public class GlobalExceptionHandler{
    9. /**
    10. * 自定义异常拦截器
    11. * @param req
    12. * @param e
    13. * @return {@link Result}
    14. */
    15. @ResponseBody
    16. @ExceptionHandler(value =ServiceException.class)
    17. public Result exceptionHandler(HttpServletRequest req, ServiceException e){
    18. log.info("发送{}异常",e.getMessage());
    19. return Result.getBusinessException(e.getLocalizedMessage(),e.getCode());
    20. }
    21. @ResponseBody
    22. @ExceptionHandler(value =Exception.class)
    23. public Result exceptionHandler(HttpServletRequest req, Exception e){
    24. log.info("发送{}异常",e.getMessage());
    25. return Result.getBusinessException(e.getLocalizedMessage());
    26. }
    27. }

    Server层实现:

    1. @Service
    2. @Slf4j
    3. public class LoginServiceImpl implements ILoginService {
    4. @Resource
    5. UserMapper userMapper;
    6. @Resource
    7. TokenUtil tokenUtil;
    8. @Resource
    9. UserRoleMapper userRoleMapper;
    10. /**
    11. * 登录实现
    12. * @param loginDto
    13. * @return {@link LoginVo}
    14. */
    15. @Override
    16. public LoginVo login(LoginDto loginDto) {
    17. UserPo userPo = userMapper.selectOne(new LambdaQueryWrapper<UserPo>().eq(UserPo::getAccount, loginDto.getAccount()));
    18. if (userPo!=null){
    19. if (userPo.getPassword().equals(loginDto.getPassword())){
    20. /**
    21. * 构建token
    22. */
    23. String token = tokenUtil.buildToken(userPo.getAccount());
    24. LoginVo loginVo = new LoginVo();
    25. loginVo.setToken(token);
    26. loginVo.setRoleId(userRoleMapper.selectOne(
    27. new LambdaQueryWrapper<UserRolePo>().eq(UserRolePo::getUserId,userPo.getUserId())).getRoleId());
    28. loginVo.setAccount(userPo.getAccount());
    29. return loginVo;
    30. }else{
    31. throw new ServiceException("422","密码错误");
    32. }
    33. }else {
    34. throw new ServiceException("422", "用户不存在");
    35. }
    36. }
    37. }

    后面的Controller、Mapper、实体层大家要根据自己的需求字段来进行具体实现,我就不贴出来了。

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

    如图所示:

                                    

  • 相关阅读:
    Sentinel滑动时间窗口的原理及源码实现
    爬虫入门基础与Selenium反爬虫策略
    正向代理与反向代理
    OpenAI“杀疯了”,GPT–4o模型保姆级使用教程!一遍就会!
    CentOS 7.9如何禁止内核自动更新升级
    Kubernetes多租户策略的好处和挑战
    【洛谷算法题】P5707-上学迟到【入门1顺序结构】
    计算机二级WPS 选择题(模拟和解析四)
    手把手教你音乐服务器搭建
    Nginx可视化管理工具结合cpolar实现远程访问内网服务
  • 原文地址:https://blog.csdn.net/qq_64680177/article/details/133615590