用JDK8用了差不多十年了,也该更新一下了,毕竟SpringBoot3系列已经不再支持JDK8了,所以本次的整合是基于JDK17的。

SpringBoot3.1.2整合Security时,Security的默认版本为6.1.2。

废话不多说,直接上代码。

(response.write封装我不贴了,这种基础代码自己解决就好)

依赖:pom.xml:

        <!--spring security 启动器-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
            <version>3.1.2</version>
        </dependency>
        <!-- https://mvnrepository.com/artifact/javax.xml.bind/jaxb-api -->
        <dependency>
            <groupId>javax.xml.bind</groupId>
            <artifactId>jaxb-api</artifactId>
            <version>2.3.1</version>
        </dependency>
        <!--jwt-->
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
            <version>0.9.0</version>
        </dependency>
        <!-- mybatis -->
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>3.0.0</version>
        </dependency>
        <!-- mysql -->
        <dependency>
            <groupId>com.mysql</groupId>
            <artifactId>mysql-connector-j</artifactId>
            <scope>runtime</scope>
        </dependency>
        <!--redis-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>

配置文件application.yml:

server:
  port: 8080

spring:
  # 数据源配置
  datasource:
    url: jdbc:mysql://localhost:3306/hkbea-gbt?useUnicode=true&characterEncoding=UTF-8
    username: root
    password: 123456
    driver-class-name: com.mysql.cj.jdbc.Driver


# mybatis配置
mybatis:
  # 配置SQL映射文件路径
  mapper-locations: classpath:/mapper/*.xml
  # 实体类别名包扫描(请更换成实际的)
  type-aliases-package: **.**.**.**
  # 驼峰命名
  configuration:
    map-underscore-to-camel-case: true

token异常处理类:AuthenticationEntryPointImpl.java

import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;

import java.io.IOException;

/**
 * 认证失败
 */
@Component
public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint {
    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException {
        response.setContentType("application/json;charset=UTF-8");
        response.getWriter().println("无效登录信息");
    }
}
permission异常处理类:AccessDeniedHandlerImpl.java
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.stereotype.Component;

import java.io.IOException;

@Component
public class AccessDeniedHandlerImpl implements AccessDeniedHandler {
    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException {
        response.setContentType("application/json;charset=UTF-8");
        response.getWriter().println("你无权进行本操作");
    }
}

核心配置类:SecurityConfig.java

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

/**
 * SpringSecurity 核心配置类
 * prePostEnabled = true 开启注解权限认证功能
 */
@Configuration
@EnableWebSecurity
@EnableMethodSecurity
public class SecurityConfig {

    @Autowired
    private AuthenticationConfiguration authenticationConfiguration;

    //认证过滤器
    @Autowired
    private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;

    // 认证失败处理器
    @Autowired
    private AuthenticationEntryPointImpl authenticationEntryPoint;

    // 授权失败处理器
    @Autowired
    private AccessDeniedHandlerImpl accessDeniedHandler;

    /**
     * 认证配置
     * anonymous():匿名访问:未登录可以访问,已登录不能访问
     * permitAll():有没有认证都能访问:登录或未登录都能访问
     * denyAll(): 拒绝
     * authenticated():认证之后才能访问
     * hasAuthority():包含权限
     */
    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {

        http
            // 允许跨域(也可以不允许,一般前后端分离项目要关闭此功能)
            .cors()
            //关闭csrf:为了防止通过伪造用户请求来访问受信用站点的非法请求访问
            .and()
                .csrf().disable();
        http
            .anonymous().disable()  //禁用anonymous用户(影响Authentication.getPrincipal()的返回):disable=>匿名用户的authentication为null
            // 禁用session (前后端分离项目,不通过Session获取SecurityContext)
            .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
            //登录配置  //此处全部注释,在本案例中采用注解进行权限拦截
            //.and()
                // 添加token过滤器
                //.apply(permitAllSecurityConfig)
                //.and()
                // 配置路径是否需要认证
                //.authorizeHttpRequests()
                    //配置放行资源(注意: 放行资源必须放在所有认证请求之前!)
                    //.requestMatchers("/api/v1/auth/login").permitAll()
                    //.requestMatchers("/api/v1/auth/register").permitAll()
                    //.requestMatchers("/api/v1/on1on/**").permitAll()
                    //.requestMatchers("/api/v1/admin/**").hasAuthority("admin")  // 对于admin接口 需要权限admin
                    // 除上面外的所有请求全部需要鉴权认证
                    //.anyRequest().authenticated()   //代表所有请求,必须认证之后才能访问
                    //.anyRequest().permitAll()
            .and()
                .authenticationManager(authenticationManager(authenticationConfiguration))
                .sessionManagement()
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
                //此处为添加jwt过滤
                .addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class)
        ;

        // 配置异常处理器
        http.exceptionHandling()
                // 认证失败
                .authenticationEntryPoint(authenticationEntryPoint)
                // 授权失败
                .accessDeniedHandler(accessDeniedHandler);


        http.headers().frameOptions().disable();

        return http.build();
    }

    /**
     * 注入AuthenticationManager 进行用户认证(基于用户名和密码或使用用户名和密码进行身份验证)
     * @param authenticationConfiguration
     * @return
     * @throws Exception
     */
    @Bean
    public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {
        return authenticationConfiguration.getAuthenticationManager();
    }

    /**
     * 提供密码机密处理机制:
     * 将BCryptPasswordEncoder对象注入到spring容器中,更换掉原来的 PasswordEncoder加密方式
     * 原PasswordEncoder密码格式为:{id}password。它会根据id去判断密码的加密方式。
     * 如果没替换原来的加密方式,数据库中想用明文密码做测试,将密码字段改为{noop}123456这样的格式
     */
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

}

RBAC中的Account.java:

import lombok.Data;

import java.io.Serializable;

@Data
public class Account implements Serializable {

    private String id;
    private String username;
    private String password;
    private String nickname;

}

RBAC中的AccountDao:AccountDao.java

import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;

@Mapper
public interface AccountDao {
    /**
     * 获取用户信息
     */
    @Select("select * from account where username = #{username}")
    Account getUserInfo(String username);
}

RBAC中的AccountService接口:AccountService.java

public interface AccountService {

    /**
     * 根据用户名获取用户信息
     */
    Account getUserInfo(String username);
}

RBAC中的AccountService接口实现类:AccountServiceImpl.java

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class AccountServiceImpl implements AccountService {

    @Autowired
    private AccountDao accountDao;

    /**
     * 根据用户名获取用户信息
     */
    @Override
    public Account getUserInfo(String username) {
        return accountDao.getUserInfo(username);
    }
}

RBAC中的权限查询类:AuthDao.java

import org.apache.ibatis.annotations.Mapper;

import java.util.List;
import java.util.Map;

@Mapper
public interface AuthDao {

    List<Map<String,Object>> getRolesAndPermissionsByAccountId(String id);

}

RBAC中的权限查询Mapper:AuthMapper.xml

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.hkbea.gbt.dao.AuthDao">

    <!--通过手机号查找用户权限信息-->
    <select id="getRolesAndPermissionsByAccountId" resultType="java.util.HashMap" parameterType="String">
        SELECT role.name AS role, permission.name AS permission
        FROM role,
             permission,
             account_role,
             role_permission
        WHERE account_role.account_id=#{id}
          AND account_role.role_id=role.id
          AND role_permission.role_id=role.id
          AND role_permission.permission_id=permission.id
    </select>

</mapper>
SpringSecurity核心的用户类:LoginUser.java
import com.alibaba.fastjson2.annotation.JSONField;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.extern.log4j.Log4j2;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

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

/**
 * 重写UserDetails返回的用户信息
 * SpringSecurity返回的用户信息实体类
 */
@Log4j2
@Data
@NoArgsConstructor
public class LoginUser implements UserDetails {

    private Account account;    //用户信息

    private List<Map<String,Object>> rolesAndPermissions; //角色信息

    private List<String> roles; //角色信息

    private List<String> permissions;   //权限信息

    public LoginUser(Account account, List<Map<String,Object>> rolesAndPermissions) {
        this.account = account;
        this.roles = new ArrayList<>();
        this.permissions = new ArrayList<>();
        for (Map<String,Object> map : rolesAndPermissions){
            log.info(map);
            log.info("role : {}",map.get("role"));
            this.roles.add((String)map.get("role"));
            log.info("permission : {}",map.get("permission"));
            this.permissions.add((String)map.get("permission"));
        }
    }

    @JSONField(serialize = false) //fastjson注解,表示此属性不会被序列化,因为SimpleGrantedAuthority这个类型不能在redis中序列化
    private List<SimpleGrantedAuthority> authorities;

    /**
     * 获取权限信息
     */
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        // 权限为空的时候才往遍历,不为空直接返回
        if (authorities != null) {
            return authorities;
        }
        //把permissions中String类型的权限信息封装成SimpleGrantedAuthority对象
        authorities = new ArrayList<>();
        for (String permission : permissions) {
            SimpleGrantedAuthority authority = new SimpleGrantedAuthority(permission);
            authorities.add(authority);
        }
        return authorities;
    }

    /**
     * 获取密码
     */
    @Override
    public String getPassword() {
        return account.getPassword();
    }

    /**
     * 获取用户名
     */
    @Override
    public String getUsername() {
        return account.getUsername();
    }

    /**
     * 判断是否过期(账户是否未过期,过期无法验证)
     */
    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    /**
     * 是否锁定(指定用户是否解锁,锁定的用户无法进行身份验证)
     */
    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    /**
     * 是否没有超时(指示是否已过期的用户的凭据(密码),过期的凭据防止认证)
     */
    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    /**
     * 是否可用(用户是否被启用或禁用。禁用的用户无法进行身份验证。)
     */
    @Override
    public boolean isEnabled() {
        return true;
    }

}

Redis使用FastJson序列化的相关类:FastJsonRedisSerializer.java

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.ParserConfig;
import com.alibaba.fastjson.serializer.SerializerFeature;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.type.TypeFactory;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.SerializationException;

import java.nio.charset.Charset;

/**
 * Redis使用FastJson序列化
 */
public class FastJsonRedisSerializer<T> implements RedisSerializer<T> {

    public static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8");

    private Class<T> clazz;

    static {
        ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
    }

    public FastJsonRedisSerializer(Class<T> clazz) {
        super();
        this.clazz = clazz;
    }

    @Override
    public byte[] serialize(T t) throws SerializationException {
        if (t == null) {
            return new byte[0];
        }
        return JSON.toJSONString(t, SerializerFeature.WriteClassName).getBytes(DEFAULT_CHARSET);
    }

    @Override
    public T deserialize(byte[] bytes) throws SerializationException {
        if (bytes == null || bytes.length <= 0) {
            return null;
        }
        String str = new String(bytes, DEFAULT_CHARSET);

        return JSON.parseObject(str, clazz);
    }


    protected JavaType getJavaType(Class<?> clazz) {
        return TypeFactory.defaultInstance().constructType(clazz);
    }
}

Redis配置类:RedisConfig.java

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.StringRedisSerializer;

/**
 * redis配置类
 * 避免存入redis中的key看上去乱码的现象
 */
@Configuration
public class RedisConfig {

    @Bean
    @SuppressWarnings(value = {"unchecked", "rawtypes"})
    public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
        RedisTemplate<Object, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(connectionFactory);

        //使用FastJsonRedisSerializer来序列化和反序列化redis的value值
        FastJsonRedisSerializer serializer = new FastJsonRedisSerializer(Object.class);

        // 使用StringRedisSerializer来序列化和反序列化redis的key值
        template.setKeySerializer(new StringRedisSerializer());
        template.setValueSerializer(serializer);

        // Hash的key也采用StringRedisSerializer的序列化方式
        template.setHashKeySerializer(new StringRedisSerializer());
        template.setHashValueSerializer(serializer);

        template.afterPropertiesSet();
        return template;
    }
}

Redis工具类:RedisCache.java

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.BoundSetOperations;
import org.springframework.data.redis.core.HashOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.stereotype.Component;

import java.util.*;
import java.util.concurrent.TimeUnit;

/**
 * redis工具类
 */
@Component
public class RedisCache {
    @Autowired
    public RedisTemplate redisTemplate;

    /**
     * 缓存基本的对象,Integer、String、实体类等
     *
     * @param key   缓存的键值
     * @param value 缓存的值
     */
    public <T> void setCacheObject(final String key, final T value) {
        redisTemplate.opsForValue().set(key, value);
    }

    /**
     * 缓存基本的对象,Integer、String、实体类等
     *
     * @param key      缓存的键值
     * @param value    缓存的值
     * @param timeout  时间
     * @param timeUnit 时间颗粒度
     */
    public <T> void setCacheObject(final String key, final T value, final Integer timeout, final TimeUnit timeUnit) {
        redisTemplate.opsForValue().set(key, value, timeout, timeUnit);
    }

    /**
     * 设置有效时间
     *
     * @param key     Redis键
     * @param timeout 超时时间
     * @return true=设置成功;false=设置失败
     */
    public boolean expire(final String key, final long timeout) {
        return expire(key, timeout, TimeUnit.SECONDS);
    }

    /**
     * 设置有效时间
     *
     * @param key     Redis键
     * @param timeout 超时时间
     * @param unit    时间单位
     * @return true=设置成功;false=设置失败
     */
    public boolean expire(final String key, final long timeout, final TimeUnit unit) {
        return redisTemplate.expire(key, timeout, unit);
    }

    /**
     * 获得缓存的基本对象。
     *
     * @param key 缓存键值
     * @return 缓存键值对应的数据
     */
    public <T> T getCacheObject(final String key) {
        ValueOperations<String, T> operation = redisTemplate.opsForValue();
        return operation.get(key);
    }

    /**
     * 删除单个对象
     *
     * @param key
     */
    public boolean deleteObject(final String key) {
        return redisTemplate.delete(key);
    }

    /**
     * 删除集合对象
     *
     * @param collection 多个对象
     * @return
     */
    public long deleteObject(final Collection collection) {
        return redisTemplate.delete(collection);
    }

    /**
     * 缓存List数据
     *
     * @param key      缓存的键值
     * @param dataList 待缓存的List数据
     * @return 缓存的对象
     */
    public <T> long setCacheList(final String key, final List<T> dataList) {
        Long count = redisTemplate.opsForList().rightPushAll(key, dataList);
        return count == null ? 0 : count;
    }

    /**
     * 获得缓存的list对象
     *
     * @param key 缓存的键值
     * @return 缓存键值对应的数据
     */
    public <T> List<T> getCacheList(final String key) {
        return redisTemplate.opsForList().range(key, 0, -1);
    }

    /**
     * 缓存Set
     *
     * @param key     缓存键值
     * @param dataSet 缓存的数据
     * @return 缓存数据的对象
     */
    public <T> BoundSetOperations<String, T> setCacheSet(final String key, final Set<T> dataSet) {
        BoundSetOperations<String, T> setOperation = redisTemplate.boundSetOps(key);
        Iterator<T> it = dataSet.iterator();
        while (it.hasNext()) {
            setOperation.add(it.next());
        }
        return setOperation;
    }

    /**
     * 获得缓存的set
     *
     * @param key
     * @return
     */
    public <T> Set<T> getCacheSet(final String key) {
        return redisTemplate.opsForSet().members(key);
    }

    /**
     * 缓存Map
     *
     * @param key
     * @param dataMap
     */
    public <T> void setCacheMap(final String key, final Map<String, T> dataMap) {
        if (dataMap != null) {
            redisTemplate.opsForHash().putAll(key, dataMap);
        }
    }

    /**
     * 获得缓存的Map
     *
     * @param key
     * @return
     */
    public <T> Map<String, T> getCacheMap(final String key) {
        return redisTemplate.opsForHash().entries(key);
    }

    /**
     * 往Hash中存入数据
     *
     * @param key   Redis键
     * @param hKey  Hash键
     * @param value 值
     */
    public <T> void setCacheMapValue(final String key, final String hKey, final T value) {
        redisTemplate.opsForHash().put(key, hKey, value);
    }

    /**
     * 获取Hash中的数据
     *
     * @param key  Redis键
     * @param hKey Hash键
     * @return Hash中的对象
     */
    public <T> T getCacheMapValue(final String key, final String hKey) {
        HashOperations<String, String, T> opsForHash = redisTemplate.opsForHash();
        return opsForHash.get(key, hKey);
    }

    /**
     * 删除Hash中的数据
     *
     * @param key
     * @param hkey
     */
    public void delCacheMapValue(final String key, final String hkey) {
        HashOperations hashOperations = redisTemplate.opsForHash();
        hashOperations.delete(key, hkey);
    }

    /**
     * 获取多个Hash中的数据
     *
     * @param key   Redis键
     * @param hKeys Hash键集合
     * @return Hash对象集合
     */
    public <T> List<T> getMultiCacheMapValue(final String key, final Collection<Object> hKeys) {
        return redisTemplate.opsForHash().multiGet(key, hKeys);
    }

    /**
     * 获得缓存的基本对象列表
     *
     * @param pattern 字符串前缀
     * @return 对象列表
     */
    public Collection<String> keys(final String pattern) {
        return redisTemplate.keys(pattern);
    }
}

JWT工具类:JwtTokenUtil.java

import io.jsonwebtoken.*;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Configuration;
import org.springframework.stereotype.Component;

import java.util.Date;
import java.util.HashMap;
import java.util.Map;

@Configuration
@Component
@Slf4j
public class JwtTokenUtil {

    /**
     * token的头key
     */
    public static final String AUTH_HEADER_KEY = "Authorization";
    /**
     * token前缀
     */
    public static final String TOKEN_PREFIX = "Bearer ";

    /**
     * token 过期时间 30分钟
     */
    public static final long EXPIRATION = 1000 * 60 * 30;

    /**
     * 加密的key(自己生成的任意值)
     */
    public static final String APP_SECRET_KEY = "secret";

    /**
     * 生成token
     * @return token字符串
     */
    public static String createToken(Account account) {

        Map<String, Object> claims = new HashMap<>();
        claims.put("id", account.getId());
        claims.put("username", account.getUsername());
        claims.put("nickname", account.getNickname());

        String token = Jwts
                .builder()
                .setId(account.getId())
                .setClaims(claims)
                .setIssuedAt(new Date())
                .setExpiration(new Date(System.currentTimeMillis() + EXPIRATION))
                .signWith(SignatureAlgorithm.HS256, APP_SECRET_KEY)
                .compact();
        return TOKEN_PREFIX +token;
    }

    /**
     * 解析jwt
     * @return 解析后的JWT Claims
     */
    public static Claims parseJwt(String token) {
        try {
            return Jwts.parser().setSigningKey(APP_SECRET_KEY).parseClaimsJws(token.replace(TOKEN_PREFIX, "")).getBody();
        } catch (Exception e) {
            log.error("token exception : ", e);
        }
        return null;
    }

    /**
     * 获取当前登录用户的ID
     *
     * @param token
     * @return
     */
    public static String getAccountId(String token) {
        Claims claims = parseJwt(token);
        return (String)claims.get("id");
    }

    /**
     * 获取当前登录用户用户名
     *
     * @param token
     * @return
     */
    public static String getUsername(String token) {
        Claims claims = parseJwt(token);
        return (String)claims.get("username");
    }

    /**
     * 检查token是否过期
     *
     * @param  token token
     * @return boolean
     */
    public static boolean isExpiration(String token) {
        Claims claims = parseJwt(token);
        return claims.getExpiration().before(new Date());
    }


    public boolean validateToken(String token) {
        try {
            Jwts.parser().setSigningKey(APP_SECRET_KEY).parseClaimsJws(token);
            return true;
        } catch (SignatureException e) {
            log.error("Invalid JWT signature: {}", e.getMessage());
        } catch (MalformedJwtException e) {
            log.error("Invalid JWT token: {}", e.getMessage());
        } catch (ExpiredJwtException e) {
            log.error("JWT token is expired: {}", e.getMessage());
        } catch (UnsupportedJwtException e) {
            log.error("JWT token is unsupported: {}", e.getMessage());
        } catch (IllegalArgumentException e) {
            log.error("JWT claims string is empty: {}", e.getMessage());
        }
        return false;
    }

}

JWT权限拦截过滤器:JwtAuthenticationTokenFilter.java

import com.alibaba.fastjson.JSONObject;
import io.jsonwebtoken.Claims;
import jakarta.annotation.Resource;
import jakarta.servlet.FilterChain;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.log4j.Log4j2;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;

import java.util.Objects;

/**
 * token认证过滤器
 * 作用:解析请求头中的token。并验证合法性
 * 继承 OncePerRequestFilter 保证请求经过过滤器一次
 */
@Log4j2
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {

    @Resource
    private RedisCache redisCache;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain){

        String token = request.getHeader(JwtTokenUtil.AUTH_HEADER_KEY);

        if (StringUtils.hasText(token)) {
            Claims claims = JwtTokenUtil.parseJwt(token);
            if(claims!=null){
                String accountId = (String)claims.get("id");
                String redisKey = "login:" + accountId;
                // 先转成JSON对象
                JSONObject jsonObject = redisCache.getCacheObject(redisKey);
                // JSON对象转换成Java对象
                LoginUser loginUser = jsonObject.toJavaObject(LoginUser.class);

                // redis中用户不存在
                if (Objects.isNull(loginUser)) {
                    throw new RuntimeException("redis中用户不存在!");
                }

                // 将Authentication对象(用户信息、已认证状态、权限信息)存入 SecurityContextHolder
                UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginUser, null, loginUser.getAuthorities());
                SecurityContextHolder.getContext().setAuthentication(authenticationToken);
            }
        }

        log.info("authentication :{}",SecurityContextHolder.getContext().getAuthentication());

        //放行
        try {
            filterChain.doFilter(request, response);    //放行,因为后面的会抛出相应的异常
        }catch (Exception e){
            e.printStackTrace();
        }

    }
}

鉴权Controller类:AuthApiControllerV1.java

import com.alibaba.fastjson2.JSONObject;
import com.hkbea.gbt.controller.api.v1.util.ApiControllerExceptionV1;
import com.hkbea.gbt.controller.api.v1.util.ResultVo;
import com.hkbea.gbt.model.Account;
import com.hkbea.gbt.on1on.On1onApiUtil;
import com.hkbea.gbt.security.core.LoginUser;
import com.hkbea.gbt.security.service.LoginService;
import com.hkbea.gbt.service.AccountService;
import jakarta.annotation.security.PermitAll;
import lombok.extern.log4j.Log4j2;
import org.apache.commons.lang.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.Authentication;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.*;

import java.util.UUID;

@Log4j2
@RestController
@RequestMapping("/api/v1/auth")
public class AuthApiControllerV1 {

    @Autowired
    private LoginService loginService;

    @Autowired
    private AccountService accountService;

    @PostMapping("login")
    @PermitAll
    public ResultVo login(@RequestBody JSONObject body) throws ApiControllerExceptionV1 {

        String username = body.getString("username");
        String password = body.getString("password");

        if(username==null||(username=username.trim()).equals("")){
            throw new ApiControllerExceptionV1("请输入正确的email账号");
        }
        if(password==null||(password=password.trim()).equals("")){
            throw new ApiControllerExceptionV1("请输入password");
        }

        String jwt = loginService.login(username,password);
        if(StringUtils.isEmpty(jwt)){
            throw new ApiControllerExceptionV1("账号或密码错误");
        }
        return new ResultVo(ResultVo.SUCCESS,"登录成功",jwt);
    }

    @GetMapping("info")
    @PreAuthorize("authenticated")
    public ResultVo info(Authentication authentication) throws ApiControllerExceptionV1 {

        LoginUser loginUser = (LoginUser) authentication.getPrincipal();

        Account account = loginUser.getAccount();

        JSONObject out = new JSONObject();
        out.put("id",account.getId());
        out.put("username",account.getUsername());
        out.put("on1on_id",account.getOn1onId());

        return new ResultVo(out);
    }

    /**
     * 退出登录
     */
    @RequestMapping(value="logout", method = {RequestMethod.DELETE})
    @PreAuthorize("authenticated")
    public ResultVo logout() throws ApiControllerExceptionV1{
        if(loginService.logout()){
            return new ResultVo(ResultVo.SUCCESS,"注销成功");
        }
        throw new ApiControllerExceptionV1("退出登录异常");
    }
}

测试Controller:TestApiControllerV1.java

import com.alibaba.fastjson2.JSONObject;
import jakarta.annotation.security.PermitAll;
import lombok.extern.log4j.Log4j2;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.bind.annotation.*;

@Log4j2
@RestController
@RequestMapping("/api/v1/test")
public class TestApiControllerV1 {

    @GetMapping("")
    @PermitAll
    public ResultVo test(){

        log.info("test");

        JSONObject out = new JSONObject();

        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        if(authentication!=null){
            LoginUser loginUser = (LoginUser) authentication.getPrincipal();
            System.out.println("username :"+loginUser.getAccount().getUsername());
            System.out.println("凭证 :"+authentication.getCredentials());
            System.out.println("权限 :"+authentication.getAuthorities());

            out.put("username",loginUser.getAccount().getUsername());
            out.put("credentials",authentication.getCredentials());
            out.put("authorities",authentication.getAuthorities());
        }

        return new ResultVo(out);
    }

}

MySQL测试数据脚本:

SET FOREIGN_KEY_CHECKS = 0;

-- ----------------------------
-- Table structure for account
-- ----------------------------
DROP TABLE IF EXISTS `account`;
CREATE TABLE `account`  (
  `id` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
  `username` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
  `password` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
  `nickname` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;

-- ----------------------------
-- Records of account
-- ----------------------------
INSERT INTO `account` VALUES ('5a07d8d95b9a46179d10a67f3765d363', 'admin', '$2a$10$MKzkFJJLuGMWO1eL9YuS9uinjq6H4Kalf9MzDxIFPQ4.fV9IKHNCC', '管理员');
INSERT INTO `account` VALUES ('85a223c1a519463e9e8de3a096818e6a', 'user', '123456', '普通用户');

-- ----------------------------
-- Table structure for account_role
-- ----------------------------
DROP TABLE IF EXISTS `account_role`;
CREATE TABLE `account_role`  (
  `id` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
  `account_id` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
  `role_id` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;

-- ----------------------------
-- Records of account_role
-- ----------------------------
INSERT INTO `account_role` VALUES ('eb014d68525849c29a5998e7133c1db9', '5a07d8d95b9a46179d10a67f3765d363', '28fb6f155b3b49e38a7241893b8d03f2');

-- ----------------------------
-- Table structure for permission
-- ----------------------------
DROP TABLE IF EXISTS `permission`;
CREATE TABLE `permission`  (
  `id` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
  `name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;

-- ----------------------------
-- Records of permission
-- ----------------------------
INSERT INTO `permission` VALUES ('80ad0fd1296f48a8bdd98ec700947c63', 'admin_console');

-- ----------------------------
-- Table structure for role
-- ----------------------------
DROP TABLE IF EXISTS `role`;
CREATE TABLE `role`  (
  `id` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
  `name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;

-- ----------------------------
-- Records of role
-- ----------------------------
INSERT INTO `role` VALUES ('28fb6f155b3b49e38a7241893b8d03f2', '管理员');

-- ----------------------------
-- Table structure for role_permission
-- ----------------------------
DROP TABLE IF EXISTS `role_permission`;
CREATE TABLE `role_permission`  (
  `id` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
  `role_id` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
  `permission_id` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;

-- ----------------------------
-- Records of role_permission
-- ----------------------------
INSERT INTO `role_permission` VALUES ('28e1307e38124275b9e2d428adee3e0b', '28fb6f155b3b49e38a7241893b8d03f2', '80ad0fd1296f48a8bdd98ec700947c63');

SET FOREIGN_KEY_CHECKS = 1;

测试结果: