首页 > 其他分享 >SpringBoot3 登录管理实现

SpringBoot3 登录管理实现

时间:2024-08-12 11:17:02浏览次数:14  
标签:String 登录 实现 JWT 如下 token SpringBoot3 public

一、背景知识

1. 认证方案概述

有两种常见的认证方案,分别是基于Session的认证和基于Token的认证,下面逐一进行介绍

  • 基于Session

    基于Session的认证流程如下图所示

  • 该方案的特点

    • 登录用户信息保存在服务端内存中,若访问量增加,单台节点压力会较大
    • 随用户规模增大,若后台升级为集群,则需要解决集群中各服务器登录状态共享的问题。
  • 基于Token

    基于Token的认证流程如下图所示

  • 该方案的特点

    • 登录状态保存在客户端,服务器没有存储开销
    • 客户端发起的每个请求自身均携带登录状态,所以即使后台为集群,也不会面临登录状态共享的问题。

2. Token详解

本项目采用基于Token的登录方案,下面详细介绍Token这一概念。

我们所说的Token,通常指JWT(JSON Web TOKEN)。JWT是一种轻量级的安全传输方式,用于在两个实体之间传递信息,通常用于身份验证和信息传递。

JWT是一个字符串,如下图所示,该字符串由三部分组成,三部分由.分隔。三个部分分别被称为

  • header(头部)

  • payload(负载)

  • signature(签名)

    各部分的作用如下

  • Header(头部)

    Header部分是由一个JSON对象经过base64url编码得到的,这个JSON对象用于保存JWT 的类型(typ)、签名算法(alg)等元信息,例如

    {
      "alg": "HS256",
      "typ": "JWT"
    }
    
  • Payload(负载)

    也称为 Claims(声明),也是由一个JSON对象经过base64url编码得到的,用于保存要传递的具体信息。JWT规范定义了7个官方字段,如下:

    • iss (issuer):签发人
    • exp (expiration time):过期时间
    • sub (subject):主题
    • aud (audience):受众
    • nbf (Not Before):生效时间
    • iat (Issued At):签发时间
    • jti (JWT ID):编号

    除此之外,我们还可以自定义任何字段,例如

    {
      "sub": "1234567890",
      "name": "John Doe",
      "iat": 1516239022
    }
    
  • Signature(签名)

    由头部、负载和秘钥一起经过(header中指定的签名算法)计算得到的一个字符串,用于防止消息被篡改。

二、登录流程

后台管理系统的登录流程如下图所示

根据上述登录流程,可分析出,登录管理共需三个接口,分别是获取图形验证码登录获取登录用户个人信息,除此之外,我们还需为所有受保护的接口增加验证JWT合法性的逻辑,这一功能可通过HandlerInterceptor来实现。

三、接口开发

首先在LoginController中注入LoginService,如下

@Tag(name = "后台管理系统登录管理")
@RestController
@RequestMapping("/admin")
public class LoginController {

    @Autowired
    private LoginService service;
}

1. 获取图形验证码

  • 查看响应的数据结构

    查看web-admin模块下的com.echo.lease.web.admin.vo.login.CaptchaVo,内容如下

    @Data
    @Schema(description = "图像验证码")
    @AllArgsConstructor
    public class CaptchaVo {
    
        @Schema(description="验证码图片信息")
        private String image;
    
        @Schema(description="验证码key")
        private String key;
    }
    
  • 配置所需依赖

    • 验证码生成工具

      本项目使用开源的验证码生成工具EasyCaptcha,其支持多种类型的验证码,例如gif、中文、算术等,并且简单易用,具体内容可参考其官方文档

      common模块的pom.xml文件中增加如下内容

      <dependency>
          <groupId>com.github.whvcse</groupId>
          <artifactId>easy-captcha</artifactId>
          <version>1.6.2</version>
      </dependency>
      
    • Redis

      common模块的pom.xml中增加如下内容

      <dependency>
          <groupId>org.springframework.boot</groupId>
          <artifactId>spring-boot-starter-data-redis</artifactId>
          <version>3.0.5</version>
      </dependency>
      

      application.yml中增加如下配置

      spring:
        data:
          redis:
            host: <hostname>
            port: <port>
            database: 0
      

      注意:上述hostnameport需根据实际情况进行修改

  • 编写Controller层逻辑

    LoginController中增加如下内容

    @Operation(summary = "获取图形验证码")
    @GetMapping("login/captcha")
    public Result<CaptchaVo> getCaptcha() {
        CaptchaVo captcha = service.getCaptcha();
        return Result.ok(captcha);
    }
    
  • 编写Service层逻辑

    • LoginService中增加如下内容

      CaptchaVo getCaptcha();
      
    • LoginServiceImpl中增加如下内容

      @Autowired
      private StringRedisTemplate redisTemplate;
      
      @Override
      public CaptchaVo getCaptcha() {
          SpecCaptcha specCaptcha = new SpecCaptcha(130, 48, 4);
          specCaptcha.setCharType(Captcha.TYPE_DEFAULT);
      
          String code = specCaptcha.text().toLowerCase();
          String key = RedisConstant.ADMIN_LOGIN_PREFIX + UUID.randomUUID();
          String image = specCaptcha.toBase64();
          redisTemplate.opsForValue().set(key, code, RedisConstant.ADMIN_LOGIN_CAPTCHA_TTL_SEC, TimeUnit.SECONDS);
      
          return new CaptchaVo(image, key);
      }
      

      知识点

      • 本项目Reids中的key需遵循以下命名规范:项目名:功能模块名:其他,例如admin:login:123456

      • spring-boot-starter-data-redis已经完成了StringRedisTemplate的自动配置,我们直接注入即可。

      • 为方便管理,可以将Reids相关的一些值定义为常量,例如key的前缀、TTL时长,内容如下。大家可将这些常量统一定义在common模块下的com.echo.lease.common.constant.RedisConstant类中

        public class RedisConstant {
            public static final String ADMIN_LOGIN_PREFIX = "admin:login:";
            public static final Integer ADMIN_LOGIN_CAPTCHA_TTL_SEC = 60;
            public static final String APP_LOGIN_PREFIX = "app:login:";
            public static final Integer APP_LOGIN_CODE_RESEND_TIME_SEC = 60;
            public static final Integer APP_LOGIN_CODE_TTL_SEC = 60 * 10;
            public static final String APP_ROOM_PREFIX = "app:room:";
        }
        

2. 登录接口

  • 登录校验逻辑

    用户登录的校验逻辑分为三个主要步骤,分别是校验验证码校验用户状态校验密码,具体逻辑如下

    • 前端发送usernamepasswordcaptchaKeycaptchaCode请求登录。
    • 判断captchaCode是否为空,若为空,则直接响应验证码为空;若不为空进行下一步判断。
    • 根据captchaKey从Redis中查询之前保存的code,若查询出来的code为空,则直接响应验证码已过期;若不为空进行下一步判断。
    • 比较captchaCodecode,若不相同,则直接响应验证码不正确;若相同则进行下一步判断。
    • 根据username查询数据库,若查询结果为空,则直接响应账号不存在;若不为空则进行下一步判断。
    • 查看用户状态,判断是否被禁用,若禁用,则直接响应账号被禁;若未被禁用,则进行下一步判断。
    • 比对password和数据库中查询的密码,若不一致,则直接响应账号或密码错误,若一致则进行入最后一步。
    • 创建JWT,并响应给浏览器。
  • 接口逻辑实现

    • 查看请求数据结构

      查看web-admin模块下的com.echo.lease.web.admin.vo.login.LoginVo,具体内容如下

      @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;
      }
      
    • 配置所需依赖

      登录接口需要为登录成功的用户创建并返回JWT,本项目使用开源的JWT工具Java-JWT,配置如下,具体内容可参考官方文档

      • 引入Maven依赖

        common模块的pom.xml文件中增加如下内容

        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt-api</artifactId>
            <version>0.12.6</version>
        </dependency>
        
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt-impl</artifactId>
            <scope>runtime</scope>
            <version>0.12.6</version>
        </dependency>
        
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt-jackson</artifactId>
            <scope>runtime</scope>
            <version>0.12.6</version>
        </dependency>
        
      • 创建JWT工具类

        common模块下创建com.echo.lease.common.utils.JwtUtil工具类,内容如下

        public class JwtUtil {
        
            private static long tokenExpiration = 60 * 60 * 1000L;
            private static SecretKey tokenSignKey = Keys.hmacShaKeyFor("M0PKKI6pYGVWWfDZw90a0lTpGYX1d4AQ".getBytes());
        
        	public static String createToken(Long userId, String username) {
                return Jwts.builder().
                        subject("USER_INFO").
                        expiration(new Date(System.currentTimeMillis() + tokenExpiration)).
                        claim("userId", userId).
                        claim("username", username).
                        signWith(tokenSignKey).
                        compact();
            }
        }
        
    • 编写Controller层逻辑

      LoginController中增加如下内容

      @Operation(summary = "登录")
      @PostMapping("login")
      public Result<String> login(@RequestBody LoginVo loginVo) {
          String token = service.login(loginVo);
          return Result.ok(token);
      }
      
    • 编写Service层逻辑

      • LoginService中增加如下内容

        String login(LoginVo loginVo);
        
      • LoginServiceImpl中增加如下内容

        @Override
        public String login(LoginVo loginVo) {
            //1.判断是否输入了验证码
            if (!StringUtils.hasText(loginVo.getCaptchaCode())) {
                throw new LeaseException(ResultCodeEnum.ADMIN_CAPTCHA_CODE_NOT_FOUND);
            }
        
            //2.校验验证码
            String code = redisTemplate.opsForValue().get(loginVo.getCaptchaKey());
            if (code == null) {
                throw new LeaseException(ResultCodeEnum.ADMIN_CAPTCHA_CODE_EXPIRED);
            }
        
            if (!code.equals(loginVo.getCaptchaCode().toLowerCase())) {
                throw new LeaseException(ResultCodeEnum.ADMIN_CAPTCHA_CODE_ERROR);
            }
        
            //3.校验用户是否存在
            SystemUser systemUser = systemUserMapper.selectOneByUsername(loginVo.getUsername());
        
            if (systemUser == null) {
                throw new LeaseException(ResultCodeEnum.ADMIN_ACCOUNT_NOT_EXIST_ERROR);
            }
        
            //4.校验用户是否被禁
            if (systemUser.getStatus() == BaseStatus.DISABLE) {
                throw new LeaseException(ResultCodeEnum.ADMIN_ACCOUNT_DISABLED_ERROR);
            }
        
            //5.校验用户密码
            if (!systemUser.getPassword().equals(DigestUtils.md5Hex(loginVo.getPassword()))) {
                throw new LeaseException(ResultCodeEnum.ADMIN_ACCOUNT_ERROR);
            }
        
            //6.创建并返回TOKEN
            return JwtUtil.createToken(systemUser.getId(), systemUser.getUsername());
        }
        
    • 编写Mapper层逻辑

      • LoginMapper中增加如下内容

        SystemUser selectOneByUsername(String username);
        
      • LoginMapper.xml中增加如下内容

        <select id="selectOneByUsername" resultType="com.echo.lease.model.entity.SystemUser">
            select id,
                   username,
                   password,
                   name,
                   type,
                   phone,
                   avatar_url,
                   additional_info,
                   post_id,
                   status
            from system_user
            where is_deleted = 0
              and username = #{username}
        </select>
        
    • 编写HandlerInterceptor

      我们需要为所有受保护的接口增加校验JWT合法性的逻辑。具体实现如下

      • JwtUtil中增加parseToken方法,内容如下

        public static Claims parseToken(String token) {
            if (token == null) {
                throw new LeaseException(ResultCodeEnum.ADMIN_LOGIN_AUTH);
            }
            try {
                return Jwts.parser()
                        .verifyWith(tokenSignKey)
                        .build()
                        .parseSignedClaims(token)
                        .getPayload();
            } catch (ExpiredJwtException e) {
                throw new LeaseException(ResultCodeEnum.TOKEN_EXPIRED);
            } catch (JwtException e) {
                throw new LeaseException(ResultCodeEnum.TOKEN_INVALID);
            }
        }
        
      • 编写HandlerInterceptor

        web-admin模块中创建com.echo.lease.web.admin.custom.interceptor.AuthenticationInterceptor类,内容如下,有关HanderInterceptor的相关内容,可参考官方文档

        @Component
        public class AuthenticationInterceptor implements HandlerInterceptor {
        
            @Override
            public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
                // Header 中获取 token
                String token = request.getHeader("Authorization");
                token = StrUtil.subAfter(token, "Bearer ", true);
                Claims claims = JwtUtil.parseToken(token);
                return true;
            }
        
        }
        

        注意

        我们约定,前端登录后,后续请求都将JWT,放置于HTTP请求的Header中,其Header的key为access-token

      • 注册HandlerInterceptor

        web-admin模块com.echo.lease.web.admin.custom.config.WebMvcConfiguration中增加如下内容

        @Autowired
        private AuthenticationInterceptor authenticationInterceptor;
        
        @Override
        public void addInterceptors(InterceptorRegistry registry) {
            registry.addInterceptor(this.authenticationInterceptor).addPathPatterns("/admin/**").excludePathPatterns("/admin/login/**");
        }
        
    • Knife4j配置

      在增加上述拦截器后,为方便继续调试其他接口,可以获取一个长期有效的Token,将其配置到Knife4j的全局参数中,如下图所示。

      注意:每个接口分组需要单独配置

      刷新页面,任选一个接口进行调试,会发现发送请求时会自动携带该header,如下图所示

3. 获取登录用户个人信息

  • 查看请求和响应的数据结构

    • 响应的数据结构

      查看web-admin模块下的com.echo.lease.web.admin.vo.system.user.SystemUserInfoVo,内容如下

      @Schema(description = "基本信息")
      @Data
      public class SystemUserInfoVo {
      
          @Schema(description = "用户姓名")
          private String name;
      
          @Schema(description = "用户头像")
          private String avatarUrl;
      }
      
    • 请求的数据结构

      按理说,前端若想获取当前登录用户的个人信息,需要传递当前用户的id到后端进行查询。但是由于请求中携带的JWT中就包含了当前登录用户的id,故请求个人信息时,就无需再传递id

  • 修改JwtUtil中的parseToken方法

    由于需要从Jwt中获取用户id,因此需要为parseToken 方法增加返回值,如下

    public static Claims parseToken(String token){
    
        if (token==null){
            throw new LeaseException(ResultCodeEnum.ADMIN_LOGIN_AUTH);
        }
    
        try{
            JwtParser jwtParser = Jwts.parserBuilder().setSigningKey(secretKey).build();
            return jwtParser.parseClaimsJws(token).getBody();
        }catch (ExpiredJwtException e){
            throw new LeaseException(ResultCodeEnum.TOKEN_EXPIRED);
        }catch (JwtException e){
            throw new LeaseException(ResultCodeEnum.TOKEN_INVALID);
        }
    }
    
  • 编写ThreadLocal工具类

    理论上我们可以在Controller方法中,使用@RequestHeader获取JWT,然后在进行解析,如下

    @Operation(summary = "获取登陆用户个人信息")
    @GetMapping("info")
    public Result<SystemUserInfoVo> info(@RequestHeader("access-token") String token) {
        Claims claims = JwtUtil.parseToken(token);
        Long userId = claims.get("userId", Long.class);
        SystemUserInfoVo userInfo = service.getLoginUserInfo(userId);
        return Result.ok(userInfo);
    }
    

    上述代码的逻辑没有任何问题,但是这样做,JWT会被重复解析两次(一次在拦截器中,一次在该方法中)。为避免重复解析,通常会在拦截器将Token解析完毕后,将结果保存至ThreadLocal中,这样一来,我们便可以在整个请求的处理流程中进行访问了。

    ThreadLocal概述

    ThreadLocal的主要作用是为每个使用它的线程提供一个独立的变量副本,使每个线程都可以操作自己的变量,而不会互相干扰,其用法如下图所示。

    common模块中创建com.echo.lease.common.login.LoginUserHolder工具类

    public class LoginUserHolder {
        public static ThreadLocal<LoginUser> threadLocal = new ThreadLocal<>();
    
        public static void setLoginUser(LoginUser loginUser) {
            threadLocal.set(loginUser);
        }
    
        public static LoginUser getLoginUser() {
            return threadLocal.get();
        }
    
        public static void clear() {
            threadLocal.remove();
        }
    }
    

    同时在common模块中创建com.echo.lease.common.login.LoginUser

    @Data
    @AllArgsConstructor
    public class LoginUser {
    
        private Long userId;
        private String username;
    }
    
  • 修改AuthenticationInterceptor拦截器

    @Component
    public class AuthenticationInterceptor implements HandlerInterceptor {
    
        @Override
        public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
            // Header 中获取 token
            String token = request.getHeader("Authorization");
            token = StrUtil.subAfter(token, "Bearer ", true);
            Claims claims = JwtUtil.parseToken(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();
        }
    }
    
  • 编写Controller层逻辑

    LoginController中增加如下内容

    @Operation(summary = "获取登陆用户个人信息")
    @GetMapping("info")
    public Result<SystemUserInfoVo> info() {
        SystemUserInfoVo userInfo = service.getLoginUserInfo(LoginUserHolder.getLoginUser().getUserId());
        return Result.ok(userInfo);
    }
    
  • 编写Service层逻辑

    LoginService中增加如下内容

    @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;
    }
    

笔记整理自尚硅谷Java项目【尚庭公寓】从0开始Java项目实战

标签:String,登录,实现,JWT,如下,token,SpringBoot3,public
From: https://www.cnblogs.com/echohye/p/18354534

相关文章

  • vue实现录音并转文字功能,包括PC端web,手机端web
    vue实现录音并转文字功能,包括PC端,手机端和企业微信自建应用端不止vue,不限技术栈,vue2、vue3、react、.net以及原生js均可实现。原理浏览器实现录音并转文字最快捷的方法是通过WebSpeechAPI来实现,这是浏览器内置示例的api方法,可以直接调用,无需引入任何依赖包,唯一需要注意的是浏览......
  • SpringBoot 使用策略+工厂模式的几种实现方式
    SpringBoot使用策略+工厂模式的几种实现方式  1.方式一:  2.方式二:使用Spring依赖注入   用过Spring的肯定也离不开这个注解,通过这个注解可以帮我们自动注入我们想要的Bean。除了这个基本功能之外,@Autowired还有更加强大的功能,还可以注入指定类型的数组,Lis......
  • 前端实现刮刮卡的效果
     先来一张图看看,这是现实中的,那么我们如何在程序上实现这样的效果呢,主流方案是采用canvas来实现首先要实现这种效果,核心点是了解canvas的globalCompositeOperation属性,这个属性的作用是控制canvas上不同图层之间的显示效果,传送门我们这里只需要使用到"destination-out"属性,这......
  • python实现文字识别
    在Python中实现文字识别(OCR,OpticalCharacterRecognition)的一种流行方式是使用开源库如Tesseract。Tesseract是一个由HP实验室开发、后来由Google优化的OCR引擎,支持多种操作系统,并且能够识别多种语言的文本。步骤1:安装Tesseract首先,你需要在你的系统上安装Tesseract。......
  • nodejs+阿里云实现发送短信验证码
    一、阿里云短信服务打开阿里云短信服务控制台1.新增资质2.新增模板与签名3.等待审核通过二、node代码1.首先需要下载依赖npmi@alicloud/pop-core--save2.引入依赖关键信息建议放环境变量中 accessKeyId和accessKeySecret在这里↓ 以下是代码constC......
  • CSP真题答案《202309-01、02》基于Python的实现
    注意:注释在测试CSP时应全部删除!!!第一题:#键盘输入两个数以空格隔开,分别为n,mn,m=map(int,input().split())#根据n值可以循环输入n行值,得到一个列表(操作数)madenum=[list(map(int,input().split()))for_inrange(n)]#根据m值可以循环输入m行值,得到一个列表(初始......
  • 基于微信小程序的心理测评平台设计与实现
    基于微信小程序的心理测评平台设计与实现DesignandImplementationofaPsychologicalAssessmentPlatformbasedonWeChatMiniProgram完整下载链接:基于微信小程序的心理测评平台设计与实现文章目录基于微信小程序的心理测评平台设计与实现摘要第一章引言1.1......
  • C#窗体自定义快捷操作键的实现 - 开源研究系列文章
          这次想到应用程序的窗体的快捷操作键的使用的问题。      上次发布过一个快捷键的例子(https://www.cnblogs.com/lzhdim/p/18342051),区别在于它是操作系统全局注册的热键,如果其它应用程序注册了对应的热键,那就会失效。此例子是对某个窗体里的按键的操作进行的......
  • 基于SpringBoot框架的企业财务管理系统设计与实现(论文+源码)_kaic
    摘  要在快速增长的信息时代,每个企业都在紧随其后,不断改进其办公模式。与此同时,各家企业的传统管理模式也逐步发生变化,政府和企业都将需要一个更加自动化和现代化的财务管理系统。这能够便利员工之间的信息交流和公司的工作任务进而提高工作效率。因此对现有的系统进行调......
  • 【待做】【免杀对抗】怎样实现一个基础的shellcodeloader
    一、SHELLCODE二、API调用隐藏2.1执行API2.2隐藏IAT调用API2.3Syscall三、AntiSandBox四、反调试4.1检测进程4.2LLVM五、免杀PE文件在越来越多的攻防演练项目中,上线机器至C2(Command&Control)以及钓鱼打开内网入口点都需对其所使用的落地文件......