【工作记录】基于springboot3+springsecurity实现多种方式登录及鉴权(二)

本文介绍了如何在springboot3应用中结合springsecurity实现多种登录方式,包括用户名+密码和手机号+验证码登录。文章详细讲解了配置数据库连接、redis缓存、jwt token的生成和验证,以及登录逻辑和过滤器验证的调整。同时,添加了登出接口和登出后的访问验证。这是一个完整的用户认证流程实践。
摘要由CSDN通过智能技术生成

前言

上篇文章介绍了基于springboot3+springsecurity实现的基于模拟数据的用户多种方式登录及鉴权的流程和代码实现,本文我们继续完善。

主要完善的点

主要通过如下几个点来完成优化和完善:

  1. 用户信息获取通过查询mysql数据库实现
  2. token生成方式使用jwt
  3. 用户信息存储及读取使用redis
  4. 完善过滤器用户及token校验逻辑
  5. 添加登出接口
  6. 其他部分内容简单调整和修改

说明: 以下所有内容均在上文代码基础上进行修改,有不正确的地方欢迎留言指出。

开始

pom.xml修改

添加如下依赖:

<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>8.0.32</version>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency><!-- Mybatis-Plus的核心依赖 -->
<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-boot-starter</artifactId>
    <version>3.5.5</version> <!-- 根据实际可用版本号填写 -->
</dependency>
<dependency>
    <groupId>com.nimbusds</groupId>
    <artifactId>nimbus-jose-jwt</artifactId>
    <version>9.38-rc3</version>
</dependency>

结合上面的完善点,引入了如下jar包:

mysql-connector-java: 用于连接mysql数据库

spring-boot-starter-data-redis: 用于链接和操作redis,主要是为了存储和获取用户信息和token

mybatis-plus-spring-boot-starter: 用于操作数据库

nimbus-jose-jwt: 生成和验证jwt,当然也可以选择其他的jar包

需要注意的是如果使用的是springboot 3.2.0或者以上版本,需要修改mybatis-plus包的maven坐标, 否则在启动时会抛出异常。

<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-spring-boot3-starter</artifactId>
    <version>3.5.5</version>
</dependency>

抛出的异常信息如下:

java.lang.IllegalArgumentException: Invalid value type for attribute 'factoryBeanObjectType': java.lang.String
	at org.springframework.beans.factory.support.FactoryBeanRegistrySupport.getTypeForFactoryBeanFromAttributes(FactoryBeanRegistrySupport.java:86) ~[spring-beans-6.1.2.jar:6.1.2]
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.getTypeForFactoryBean(AbstractAutowireCapableBeanFactory.java:836) ~[spring-beans-6.1.2.jar:6.1.2]
	at org.springframework.beans.factory.support.AbstractBeanFactory.isTypeMatch(AbstractBeanFactory.java:620) ~[spring-beans-6.1.2.jar:6.1.2]
	at org.springframework.beans.factory.support.DefaultListableBeanFactory.doGetBeanNamesForType(DefaultListableBeanFactory.java:575) ~[spring-beans-6.1.2.jar:6.1.2]
	at org.springframework.beans.factory.support.DefaultListableBeanFactory.getBeanNamesForType(DefaultListableBeanFactory.java:534) ~[spring-beans-6.1.2.jar:6.1.2]
	at org.springframework.context.support.PostProcessorRegistrationDelegate.invokeBeanFactoryPostProcessors(PostProcessorRegistrationDelegate.java:138) ~[spring-context-6.1.2.jar:6.1.2]
	at org.springframework.context.support.AbstractApplicationContext.invokeBeanFactoryPostProcessors(AbstractApplicationContext.java:789) ~[spring-context-6.1.2.jar:6.1.2]
	at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:606) ~[spring-context-6.1.2.jar:6.1.2]
	at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.refresh(ServletWebServerApplicationContext.java:146) ~[spring-boot-3.2.1.jar:3.2.1]
	at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:762) ~[spring-boot-3.2.1.jar:3.2.1]
	at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:464) ~[spring-boot-3.2.1.jar:3.2.1]
	at org.springframework.boot.SpringApplication.run(SpringApplication.java:334) ~[spring-boot-3.2.1.jar:3.2.1]
	at org.springframework.boot.SpringApplication.run(SpringApplication.java:1358) ~[spring-boot-3.2.1.jar:3.2.1]
	at org.springframework.boot.SpringApplication.run(SpringApplication.java:1347) ~[spring-boot-3.2.1.jar:3.2.1]
	at com.zjtx.tech.security.demo.SecurityDemoApplication.main(SecurityDemoApplication.java:10) ~[classes/:na]

添加配置

新建application.yml

spring:
  datasource:
    url: jdbc:mysql://替换成实际数据库ip:端口/security_demo?serverTimezone=UTC&useSSL=false
    username: 实际数据库用户名
    password: 实际数据库密码
    driver-class-name: com.mysql.cj.jdbc.Driver
    hikari:
      poolName: Hikari
  data:
    redis:
      host: 替换成redis的host
      port: 替换成redis的端口
      database: 11
      timeout: 10000
      jedis:
        pool:
          maxIdle: 10
          minIdle: 1
          enabled: true
          max-wait: 10000ms
# 以下配置可根据实际情况自行修改
mybatis-plus:
  mapper-locations: classpath:mapper/*.xml
  configuration:
    map-underscore-to-camel-case: true
  global-config:
    db-config:
      table-prefix: t_
      table-underline: true
      id-type: assign_uuid

主要就是配置mysql、redis、mybatis-plus,比较简单易懂,根据实际情况修改即可。

添加工具类

jwt生成和验证的工具类 JwtTokenUtil.java

package com.zjtx.tech.security.demo.util;

import com.nimbusds.jose.JWSAlgorithm;
import com.nimbusds.jose.JWSHeader;
import com.nimbusds.jose.JWSSigner;
import com.nimbusds.jose.JWSVerifier;
import com.nimbusds.jose.crypto.MACSigner;
import com.nimbusds.jose.crypto.MACVerifier;
import com.nimbusds.jwt.JWTClaimsSet;
import com.nimbusds.jwt.SignedJWT;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.authentication.BadCredentialsException;

import java.security.SecureRandom;
import java.util.Base64;
import java.util.Date;

@Slf4j
public class JwtTokenUtils {

    /**
     * 创建一个Token
     * @param userId 用户ID
     * @return 生成的Token字符串
     * @throws Exception 如果创建Token过程中发生异常
     */
    public static String createToken(String userId)
                    throws Exception {
        JWSSigner signer = new MACSigner(Constants.TOKEN_JWT_SECRET_KEY);

        // 设置 JWT Claim Set(声明集)
        JWTClaimsSet claimsSet = new JWTClaimsSet.Builder()
                .issuer(Constants.TOKEN_ISSUER)
                .subject(userId)
                .audience("client_" + userId)
                .issueTime(new Date())
                .expirationTime(new Date(System.currentTimeMillis() + Constants.JWT_EXPIRE * 1000L)) // 有效期为7天,其实是最长7天,token不自动续期
                .notBeforeTime(new Date())
                .build();

        // 创建 SignedJWT 对象
        SignedJWT signedJwt = new SignedJWT(new JWSHeader.Builder(JWSAlgorithm.HS256).build(), claimsSet);

        // 使用签名器进行签名
        signedJwt.sign(signer);

        // 输出已签名的 JWT
        log.info("Generated JWT: {}", signedJwt.serialize());
        return signedJwt.serialize();
    }

    /**
     * 检查Token是否有效
     * @param token 要检查的Token
     * @return Token的主体
     * @throws Exception 如果解析Token、验证签名或验证Token其他条件出现错误
     */
    public static String checkToken(String token) throws Exception {
        // 解析JWT
        SignedJWT signedJwt = SignedJWT.parse(token);
        // 创建一个HMAC验证器,这里使用HS256算法,密钥与签名时相同
        JWSVerifier verifier = new MACVerifier(Constants.TOKEN_JWT_SECRET_KEY);
        // 验证签名是否有效
        if (signedJwt.verify(verifier)) {
            // 解析JWT中的声明集
            JWTClaimsSet claimsSet = signedJwt.getJWTClaimsSet();
            if(!claimsSet.getIssuer().equals(Constants.TOKEN_ISSUER)) {
                throw new BadCredentialsException("token无效,请重新登录");
            }
            // 验证过期时间和其他条件(如果需要)
            Date expirationTime = claimsSet.getExpirationTime();
            if (expirationTime != null && !expirationTime.after(new Date())) {
                throw new BadCredentialsException("token无效,请重新登录");
            }
            return claimsSet.getSubject();
        } else {
            throw new BadCredentialsException("token无效,请重新登录");
        }
    }

    /**
     * 生成一个密钥。
     * 该方法将生成一个128位(16字节)的密钥,你可以根据需要调整长度。
     * 使用`SecureRandom`生成随机字节数组作为密钥,并将其转换为Base64编码格式的字符串以便于显示和传输。
     */
    public static void generateKey() {
        // 生成一个128位(16字节)的密钥,也可以根据需要调整长度
        byte[] secretKeyBytes = new byte[64];
        SecureRandom secureRandom = new SecureRandom();
        secureRandom.nextBytes(secretKeyBytes);

        // 将密钥转换为Base64编码格式的字符串以便于显示和传输
        String secretKey = Base64.getEncoder().encodeToString(secretKeyBytes);
        log.info("Generated secret key: {}", secretKey);
    }

}

说明:

提供了三个方法,分别用于生成秘钥生成token解析token

添加RedisCache类

package com.zjtx.tech.security.demo.util;

import jakarta.annotation.Resource;
import org.springframework.data.redis.core.HashOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;

import java.util.Arrays;
import java.util.Collection;
import java.util.Map;
import java.util.concurrent.TimeUnit;

/**
 * Redis Cache
 * redis缓存工具类
 */
@Component
public class RedisCache {
    @Resource
    private RedisTemplate<String, Object> redisTemplate;

    /**
     * 默认过期时长为24小时,单位:秒
     */
    public final static long DEFAULT_EXPIRE = 60 * 60 * 24L;
    /**
     * 不设置过期时长
     */
    public final static long NOT_EXPIRE = -1L;

    public void set(String key, Object value, long expire) {
        redisTemplate.opsForValue().set(key, value);
        if (expire != NOT_EXPIRE) {
            expire(key, expire);
        }
    }

    public Long getExpire(String key){
        return redisTemplate.opsForValue().getOperations().getExpire(key);
    }

    public Long getExpire(String key, TimeUnit timeUnit){
        return redisTemplate.opsForValue().getOperations().getExpire(key, timeUnit);
    }

    public void set(String key, Object value) {
        set(key, value, DEFAULT_EXPIRE);
    }

    public Object get(String key, long expire) {
        Object value = redisTemplate.opsForValue().get(key);
        if (expire != NOT_EXPIRE) {
            expire(key, expire);
        }
        return value;
    }

    public Object get(String key) {
        return get(key, NOT_EXPIRE);
    }

    public Long increment(String key) {
        return redisTemplate.opsForValue().increment(key);
    }

    public Boolean hasKey(String key) {
        return redisTemplate.hasKey(key);
    }

    public void delete(String key) {
        redisTemplate.delete(key);
    }

    public void delete(Collection<String> keys) {
        redisTemplate.delete(keys);
    }

    public void delete(String... keys) {
        redisTemplate.delete(Arrays.asList(keys));
    }

    public Object hGet(String key, String field) {
        return redisTemplate.opsForHash().get(key, field);
    }

    public Map<String, Object> hGetAll(String key) {
        HashOperations<String, String, Object> hashOperations = redisTemplate.opsForHash();
        return hashOperations.entries(key);
    }

    public void hMSet(String key, Map<String, Object> map) {
        hMSet(key, map, DEFAULT_EXPIRE);
    }

    public void hMSet(String key, Map<String, Object> map, long expire) {
        redisTemplate.opsForHash().putAll(key, map);

        if (expire != NOT_EXPIRE) {
            expire(key, expire);
        }
    }

    public void hSet(String key, String field, Object value) {
        hSet(key, field, value, DEFAULT_EXPIRE);
    }

    public void hSet(String key, String field, Object value, long expire) {
        redisTemplate.opsForHash().put(key, field, value);

        if (expire != NOT_EXPIRE) {
            expire(key, expire);
        }
    }

    public void expire(String key, long expire) {
        redisTemplate.expire(key, expire, TimeUnit.SECONDS);
    }

    public void hDel(String key, Object... fields) {
        redisTemplate.opsForHash().delete(key, fields);
    }

    public void leftPush(String key, Object value) {
        leftPush(key, value, DEFAULT_EXPIRE);
    }

    public void leftPush(String key, Object value, long expire) {
        redisTemplate.opsForList().leftPush(key, value);

        if (expire != NOT_EXPIRE) {
            expire(key, expire);
        }
    }

    public Object rightPop(String key) {
        return redisTemplate.opsForList().rightPop(key);
    }
}

比较简单,主要就是针对redis的增删改查操作。

添加用户相关服务

之前我们是实现了SpringSecurity提供的UserDetailsService接口,本次我们扩展下这个接口及其实现。

CustomUserDetailsService.java

package com.zjtx.tech.security.demo.service.impl;

import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;

import java.util.List;

public interface CustomUserDetailsService extends UserDetailsService {

    /**
     * 根据手机号加载用户详细信息
     * @param phone 手机号
     * @return 用户详细信息
     * @throws UsernameNotFoundException 用户未找到异常
     */
    UserDetails loadUserByPhone(String phone) throws UsernameNotFoundException;

    /**
     * 根据用户ID加载用户详细信息
     * @param id 用户ID
     * @return 用户详细信息
     * @throws UsernameNotFoundException 用户未找到异常
     */
    UserDetails loadUserById(String id) throws UsernameNotFoundException;

    /**
     * 加载用户权限列表
     * @param userId 用户ID
     * @return 用户权限列表
     */
    List<String> loadUserAuthority(String userId);


}

实现类MyUserDetailsService.java

package com.zjtx.tech.security.demo.service.impl;

import com.zjtx.tech.security.demo.common.CustomUserDetails;
import com.zjtx.tech.security.demo.entity.SysUser;
import com.zjtx.tech.security.demo.mapper.SysUserMapper;
import jakarta.annotation.Resource;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;

@Service
public class MyUserDetailsService implements CustomUserDetailsService {

    @Resource
    private SysUserMapper sysUserMapper;

    @Override
    public UserDetails loadUserByUsername(String username) throws AuthenticationException {
        SysUser user = sysUserMapper.selectByUsername(username);
        return convertToUserDetails(user);
    }

    @Override
    public UserDetails loadUserByPhone(String phone) throws UsernameNotFoundException {
        SysUser user = sysUserMapper.selectByPhone(phone);
        return convertToUserDetails(user);
    }

    @Override
    public UserDetails loadUserById(String id) throws UsernameNotFoundException {
        SysUser user = sysUserMapper.selectById(id);
        return convertToUserDetails(user);
    }

    @Override
    public List<String> loadUserAuthority(String userId) {
        //FIXME 通过数据库查询用户权限,可自行完成
        return new ArrayList<>();
    }

    private UserDetails convertToUserDetails(SysUser user) {
        if(user == null) {
            throw new UsernameNotFoundException("用户不存在");
        }
        Collection<GrantedAuthority> authorities = new ArrayList<>();
        //查询系统中的用户权限
        List<String> sysAuthorities = this.loadUserAuthority(user.getId());
        if(!CollectionUtils.isEmpty(sysAuthorities)) {
            //转换成GrantedAuthority对象
            authorities.addAll(sysAuthorities.stream().map(SimpleGrantedAuthority::new).toList());
        } else {
            //添加默认角色
            GrantedAuthority defaultRole = new SimpleGrantedAuthority("common");
            GrantedAuthority xxlJobRole = new SimpleGrantedAuthority("xxl-job");
            authorities.add(defaultRole);
            authorities.add(xxlJobRole);
        }
        CustomUserDetails userDetails = new CustomUserDetails(user.getUsername(),
                user.getPassword(), authorities);
        userDetails.setId(user.getId());
        userDetails.setAge(user.getAge());
        userDetails.setSex(user.getSex());
        userDetails.setPhone(user.getPhone());
        //FIXME 用户地址 也可扩展其他属性
        userDetails.setAddress("用户默认地址,占位..........");
        return userDetails;
    }
}

其中涉及到UserMapper这个接口,是简单的数据库查询用户,这里就不展示代码了。
同时数据库表也相对简单,可以根据实体类字段推断得出,请自行创建

登录逻辑调整

用户名+密码登录调整

之前的用户名+密码登录时只是返回了token,校验时也是使用的模拟token。

本次我们生成一个jwt token返回,并存储到redis中,校验时也验证redis中的token的有效性。

修改LoginController
/**
 * 用户名密码登录
 * @param username 用户名
 * @param password 密码
 * @return 返回登录结果
 */
@GetMapping("/loginByUsernamePwd")
public Result<?> usernamePwd(String username, String password) throws Exception {
    UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(username, password);
    try{
        UsernamePasswordAuthenticationToken securityToken = (UsernamePasswordAuthenticationToken) authenticationManager.authenticate(usernamePasswordAuthenticationToken);
        CustomUserDetails userDetails = (CustomUserDetails) securityToken.getPrincipal();
        AccessTokenVo token = generateAndSaveToken(userDetails);
        return Result.ok(token);
    } catch(BadCredentialsException | UsernameNotFoundException e) {
        throw new ServerException("用户名或者密码错误");
    }
}

/**
 * 生成token并保存用户信息到redis
 * @param userDetails 登录用户信息
 * @return 返回token
 * @throws Exception 抛出非认证异常
 */
private AccessTokenVo generateAndSaveToken(CustomUserDetails userDetails) throws Exception {
    String tokenKey = Constants.getTokenKey(userDetails.getId());
    String token = JwtTokenUtils.createToken(userDetails.getId());
    redisCache.set(tokenKey, token, Constants.JWT_EXPIRE);
    String redisKey = Constants.getUserInfoKey(userDetails.getId());
    redisCache.set(redisKey, userDetails, Constants.JWT_EXPIRE);
    return new AccessTokenVo(token, userDetails.getId());
}

改动的地方就是登录成功后获取到用户信息然后根据用户id生成token并存储到redis

验证

用户名+密码登录成功后返回token
登录失败后提示异常

手机号+验证码登录调整

修改LoginController
/**
 * 获取验证码
 * @param phone 手机号
 * @return 返回登录结果
 */
@GetMapping("/getSmsCode")
public Result<?> getSmsCode(String phone) {
    if (phone == null || phone.length() != 11) {
        return Result.error("手机号码错误");
    }
    String smsCode = String.valueOf(CodeGenerator.generateIntegerCode(6));
    log.info("即将发送验证码, 验证码为: {}", smsCode);
    String codeKey = UUID.randomUUID().toString().replace("-", "");
    redisCache.set(Constants.getRedisKey(Constants.PREFIX_CODE, "login", phone + "_" + codeKey), smsCode, 5*60);
    return Result.ok(codeKey);
}

/**
 * 手机验证码登录
 * @param phone 手机号
 * @param mobileCode 验证码
 * @return 返回登录结果
*/
@GetMapping("/loginByMobileCode")
public Result<?> mobileCode(String phone, String mobileCode, String captchaKey) throws Exception {
    MobilecodeAuthenticationToken mobilecodeAuthenticationToken = new MobilecodeAuthenticationToken(phone, mobileCode, captchaKey);
    try {
        MobilecodeAuthenticationToken authenticate = (MobilecodeAuthenticationToken) authenticationManager.authenticate(mobilecodeAuthenticationToken);
        CustomUserDetails userDetails = (CustomUserDetails) authenticate.getPrincipal();
        AccessTokenVo token = generateAndSaveToken(userDetails);
        return Result.ok(token);
    } catch (BadCredentialsException | UsernameNotFoundException e) {
        throw new ServerException(e.getMessage());
    }
}
修改MobileCodeAuthenticateCodeProvider
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
    MobilecodeAuthenticationToken token = (MobilecodeAuthenticationToken) authentication;
    String phone = token.getPhone();
    String mobileCode = token.getMobileCode();
    // 判断验证码是否一致
    String redisKey = Constants.getRedisKey(Constants.PREFIX_CODE, "login", phone + "_" + token.getCaptchaKey());
    //验证码不存在 抛出异常
    if(!redisCache.hasKey(redisKey) || redisCache.get(redisKey) == null) {
        throw new BadCredentialsException("验证码已过期");
    }
    String realCode = redisCache.get(redisKey).toString();
    //验证码不一致 抛出异常
    if (!mobileCode.equals(realCode)) {
        throw new BadCredentialsException("验证码错误");
    }
    // 如果验证码一致,从数据库中读取该手机号对应的用户信息
    CustomUserDetails loadedUser = (CustomUserDetails) userDetailsService.loadUserByPhone(phone);
    MobilecodeAuthenticationToken authenticationToken = new MobilecodeAuthenticationToken(loadedUser, null, loadedUser.getAuthorities());
    //验证完成后删除验证码
    redisCache.delete(redisKey);
    return authenticationToken;
}

provider中主要调整的就是通过redis中取出验证码进行对比,同时通过mybatisplus查询数据库用户

验证

获取验证码
后台日志打印了验证码

手机号+验证码登录成功后返回token
验证码过期后的提示

过滤器验证逻辑调整

登录成功后可以拿到token,请求时需要在请求头或者参数中携带token,在后台收到请求时会在filter中进行验证和后续处理。

核心逻辑调整后代码如下:

    @Resource
    private RedisCache redisCache;

    @Resource
    private CustomUserDetailsService userDetailsService;

    @SneakyThrows
    @Override
    protected void doFilterInternal(@NonNull HttpServletRequest servletRequest,
                                    @NonNull HttpServletResponse httpServletResponse,
                                    @NonNull FilterChain filterChain) {
        String token = getToken(servletRequest);
        // 如果没有token,跳过该过滤器
        if (StringUtils.hasText(token)) {
            if(token.startsWith(Constants.TOKEN_PREFIX)) {
                token = token.replace(Constants.TOKEN_PREFIX, "");
            }
            String userId = JwtTokenUtils.checkToken(token);
            String tokenKey = Constants.getTokenKey(userId);
            // token不存在时 这个应该属于异常情况导致的,比如手动删除了redis中的数据
            if(!redisCache.hasKey(tokenKey)) {
                throw new CredentialsExpiredException("token不在缓存,请重新登录");
            }
            // 如果token在redis中,但是token不等于redis中的token,说明token被修改了,抛出异常
            if(!token.equals(redisCache.get(tokenKey))) {
                throw new CredentialsExpiredException("token不在缓存,用户在别处登录");
            }
            String userInfoKey = Constants.getUserInfoKey(userId);
            // 从redis获取token对应的用户信息
            CustomUserDetails customUserDetail = (CustomUserDetails) redisCache.get(userInfoKey);
            //如果jwt没过期但用户信息不在redis中,重新获取用户信息
            if (customUserDetail == null) {
                customUserDetail = (CustomUserDetails) userDetailsService.loadUserById(userId);
                redisCache.set(userInfoKey, customUserDetail, Constants.TOKEN_EXPIRE);
            }
            UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(customUserDetail, null, customUserDetail.getAuthorities());
            SecurityContextHolder.getContext().setAuthentication(authRequest);

            long expire = redisCache.getExpire(userInfoKey);
            //如果当前token有效期小于30分钟,更新token有效期
            if(expire <= Constants.TOKEN_REFRESH_IF_LESS_THAN) {
                log.info("检测到token小于半小时,将自动进行续期........................");
                redisCache.expire(userInfoKey, Constants.TOKEN_EXPIRE);
            }

        }
        filterChain.doFilter(servletRequest, httpServletResponse);
    }

接口访问验证

使用之前获取到的token访问接口
使用获取到的token范文接口
使用错误token或者没有token时提示异常

添加登出接口

在LoginController中添加登出接口,代码如下:

/**
 * 退出登录
 * @param accessToken JWT令牌
 * @return 登出结果
 * @throws Exception 异常
 */
@GetMapping("/logout")
public Result<?> logout(String accessToken) throws Exception {
    if(StringUtils.hasText(accessToken)) {
        String userId = JwtTokenUtils.checkToken(accessToken);
        redisCache.delete(Constants.getTokenKey(userId), Constants.getUserInfoKey(userId));
    }
    return Result.ok();
}

主要做了两件事,通过token解析出用户ID,然后清除redis中相关数据

登出及登出后的访问验证

登出接口验证
登出后再次访问提示401

测试结论

测试结果符合预期

总结

本文在上一篇文章的基础上完善了用户信息查询、token存储及校验等业务逻辑,同时引入了jwt作为token的载体。

到此一个相对完整的登录认证流程就结束了。

本文只是作为记录,如有不完善的或者有更好的想法和建议欢迎留言交流。

创作不易,欢迎一键三连。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

泽济天下

你的鼓励是我最大的动力。

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值