Spring security oauth2以redis作为tokenstore及jackson序列化失败问题

 更新时间:2024年04月15日 10:44:36   作者:紫金丨小飞侠  
这篇文章主要介绍了Spring security oauth2以redis作为tokenstore及jackson序列化失败问题,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教<BR>

前言

项目当中需要用到鉴权的场景很多,一般会使用shiro或者spring security作为一个权限验证的框架,两个框架的优缺点这里就不比较了,都是看个人习惯。

自己从搭建项目时就比较倾向于选择spring全家桶,所以就选择了spring security + oauth2的模式,一开始是使用jwt(Java-web-token)的方式,没别的,因为轻,但是慢慢后续因为功能上的需求迭代,出现了对token进行管理的需求,这才开始启用redis存储token。

一、TokenStore

顾名思义就是存储token和用来鉴权的仓库,spring自己实现了四种方案

内存存储,数据都是基于内存的,项目重启就没了

jdbc存储,管理系统用的比较多,并发吞吐不高的情况下搓搓有余了,而且坑比较少

jwt,这也就是我之前用的,好处就是token可以携带需要的信息,避免二次查询,记住不要存放敏感信息,而且RSA非对称加密的安全性也够了,缺点就是无法主动失效

我们今天要看的redis存储,其实和jdbc一样,区别在于,我速度快,哈哈哈哈

二、步骤

1.配置和代码

1.1环境

  • spring boot 2.0.9.RELEASE
  • redis 5.0.6 集群
  • mysql 8.0 + druid连接池 + mybatis

我这里用了spring cloud alibaba,nacos作为服务注册中心和配置中心了,这个不影响

  • 授权服务器
<dependency>
<!-- 指明版本,解决redis存储出现的问题:java.lang.NoSuchMethodError: org.springframework.data.redis.connection.RedisConnection.set([B[B)V问题 -->
    <groupId>org.springframework.security.oauth</groupId>
    <artifactId>spring-security-oauth2</artifactId>
    <version>2.3.3.RELEASE</version>   
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
    <groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
</dependency>
  • 资源服务器
<dependency>
    <groupId>org.springframework.security.oauth</groupId>
    <artifactId>spring-security-oauth2</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
    <groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
</dependency>

添加spring security 和 spring data redis的依赖

1.2配置文件

  • 1.2.1 授权服务器配置文件
spring:
  application:
    name: karl-auth-server
  profiles:
    active: dev
  cloud:
    nacos:
      discovery:
        server-addr: ip:8848
        namespace: public
      config:
        server-addr: ip:8848
        file-extension: yaml
        namespace: public
        group: DEFAULT_GROUP
  datasource:
    druid:
      url: jdbc:mysql://ip/database?characterEncoding=utf8&useUnicode=true&useSSL=false
      username: username
      password: password
      driver-class-name: com.mysql.cj.jdbc.Driver
      initial-size: 10
      max-active: 200
      min-idle: 5
      max-wait: 60000
      pool-prepared-statements: false
      max-pool-prepared-statement-per-connection-size: 20
      validation-query: SELECT 1 FROM DUAL
      validation-query-timeout: 30000
      test-on-borrow: false
      test-on-return: false
      test-while-idle: true
      time-between-eviction-runs-millis: 60000
      min-evictable-idle-time-millis: 300000
      filters: stat,wall,slf4j
      connection-properties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5000;config.decrypt=true;config.decrypt.key=xxxxxx;
      filter:
        config:
          enabled: true
  cache:
    type: redis
  redis:
    cluster:
      nodes:
        - ip:7001
        - ip:7002
        - ip:7003
        - ip:7004
        - ip:7005
        - ip:7006
      max-redirects: 5
    database: 0
    password: redis的密码
    timeout: 3000
    jedis:
      pool:
        min-idle: 0
        max-wait: -1
        max-idle: 30
        max-active: 10

mybatis:
  check-config-location: true

server:
  port: 8888
  • 1.2.2 资源服务器配置文件
spring:
  application:
    name: service-purchase
  #  profiles:
  #    active: dev
  cloud:
    nacos:
      discovery:
        server-addr: ip:8848
        namespace: public
      config:
        server-addr: ip:8848
        file-extension: yaml
        namespace: public
        group: DEFAULT_GROUP
  datasource:
    druid:
      url: jdbc:mysql://ip/databse?characterEncoding=utf8&useUnicode=true&useSSL=false
      username: root
      password: password
      driver-class-name: com.mysql.cj.jdbc.Driver
      initial-size: 10
      max-active: 200
      min-idle: 5
      max-wait: 60000
      pool-prepared-statements: false
      max-pool-prepared-statement-per-connection-size: 20
      validation-query: SELECT 1 FROM DUAL
      validation-query-timeout: 30000
      test-on-borrow: false
      test-on-return: false
      test-while-idle: true
      time-between-eviction-runs-millis: 60000
      min-evictable-idle-time-millis: 300000
      filters: stat,wall,slf4j
      connection-properties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5000;config.decrypt=true;config.decrypt.key=xxxxx
      filter:
        config:
          enabled: true
  cache:
    type: redis
  redis:
    cluster:
      nodes:
        - ip:7001
        - ip:7002
        - ip:7003
        - ip:7004
        - ip:7005
        - ip:7006
      max-redirects: 5
    database: 0
    password: redis的密码
    timeout: 3000
    jedis:
      pool:
        min-idle: 0
        max-wait: -1
        max-idle: 30
        max-active: 10

mybatis:
  mapper-locations: classpath:mapper/**/*Mapper.xml
  check-config-location: true

server:
  port: 8088

1.3 java代码

  • 1.3.1 授权服务器代码

首先是授权服务器的配置

@Configuration
@EnableAuthorizationServer
public class AuthServerConfiguration extends AuthorizationServerConfigurerAdapter {

    @Autowired
    public RedisConnectionFactory redisConnectionFactory;
    @Autowired
    private DataSource dataSource;
    @Autowired
    private AuthenticationManager authenticationManager;
    @Autowired
    private CustomUserDetailsServiceImpl customUserDetailsServiceImpl;

    @Bean
    public JdbcClientDetailsService customClientDetailsService() {
        JdbcClientDetailsService clientDetailsService = new JdbcClientDetailsService(dataSource);
        clientDetailsService.setPasswordEncoder(PwdUtils.ENCODER);
        return clientDetailsService;
    }

    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        clients.withClientDetails(customClientDetailsService());
    }

    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
        endpoints.authenticationManager(authenticationManager).userDetailsService(customUserDetailsServiceImpl).tokenStore(tokenStore());
        //配置TokenService参数
        DefaultTokenServices tokenService = new DefaultTokenServices();
        tokenService.setTokenStore(endpoints.getTokenStore());
        tokenService.setSupportRefreshToken(true);
        tokenService.setClientDetailsService(endpoints.getClientDetailsService());
        tokenService.setTokenEnhancer(endpoints.getTokenEnhancer());
        //token有效期 1小时
        tokenService.setAccessTokenValiditySeconds(3600);
        //token刷新有效期 15天
        tokenService.setRefreshTokenValiditySeconds(3600 * 12 * 15);
        tokenService.setReuseRefreshToken(false);
        endpoints.tokenServices(tokenService);
    }

    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) {
        security.tokenKeyAccess("isAuthenticated()")
                .checkTokenAccess("permitAll()")
                .allowFormAuthenticationForClients(); //允许接口/oauth/check_token 被调用
    }

    @Bean
    public TokenStore tokenStore() {
        RedisTokenStore redisTokenStore = new RedisTokenStore(redisConnectionFactory);
        redisTokenStore.setPrefix("karl-auth-token:");
        //自定义了jackson的序列化策略,没搞定
        //redisTokenStore.setSerializationStrategy(new Oauth2JsonSerializationStrategy());
        //JdbcTokenStore jdbcTokenStore = new JdbcTokenStore(dataSource);
        return redisTokenStore;
    }


}
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private CustomUserDetailsServiceImpl customUserDetailsServiceImpl;

    @Override
    @Bean
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }


    @Override
    protected void configure(HttpSecurity http) throws Exception {
    //放开 /oauth/** 端点
        http.csrf().disable()
                .authorizeRequests().antMatchers("/oauth/**").permitAll()
                .anyRequest().authenticated()
                .and().httpBasic();
    }

    @Bean
    public BCryptPasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();

    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(customUserDetailsServiceImpl).passwordEncoder(passwordEncoder());
    }
}
@Component
public class CustomUserDetailsServiceImpl implements UserDetailsService {

    @Autowired
    private SysUserMapper sysUserMapper;

    /**
     * 重写security的查询方法 这里需要返回username和加密后的password
     **/
    @Override
    public SysUser loadUserByUsername(String username) throws UsernameNotFoundException {
        SysUser user = sysUserMapper.selectByUsername(username);
        if (user == null) {
            throw new UsernameNotFoundException("username not found:" + username);
        }
        List<SysAuth> authorities = new ArrayList<>();
        authorities.add(new SysAuth("20200202","customer","customer"));
        user.setAuthorities(authorities);
        return user;
    }
}
  • 1.3.2 资源服务器代码
@Configuration
@EnableResourceServer
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true, jsr250Enabled = true)
public class Oauth2ResourceConfig extends ResourceServerConfigurerAdapter {

    @Autowired
    private RedisConnectionFactory redisConnectionFactory;

    @Override
    public void configure(HttpSecurity http) throws Exception {
        //关闭iframe校验
        http.headers().frameOptions().disable();
        //登陆 验证码 swagger接口及js文件
        http.csrf().disable().authorizeRequests()
                .antMatchers("/actuator/**").permitAll()
                .antMatchers("/**").authenticated();
    }

    @Override
    public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
        //无状态
        resources.stateless(true).tokenStore(tokenStore());
    }

    /**
     * 设置token存储,这一点配置要与授权服务器相一致
     */
    @Bean
    public RedisTokenStore tokenStore() {
        RedisTokenStore redisTokenStore = new RedisTokenStore(redisConnectionFactory);
        //自定义了jackson的序列化策略,没搞定
        //redisTokenStore.setSerializationStrategy(new Oauth2JsonSerializationStrategy());
        //redis key前缀
        redisTokenStore.setPrefix("karl-auth-token:");
        return redisTokenStore;
    }

2.测试

我这边用了mysql存储client信息,配置了密码和授权码的模式,这里用密码的方式测试

请求token,basic后面的是username:password的base64编码

curl --location --request POST 'http://127.0.0.1:8888/oauth/token?username=karl&password=karl&grant_type=password' \
--header 'Authorization: Basic Y2xpZW50LUE6a2FybA=='

获取到的结果是

{
    "access_token": "56526c6f-abcb-41c6-bb35-812a76e2a049",
    "token_type": "bearer",
    "refresh_token": "ac2e0962-e806-4549-af67-18edc1990d5a",
    "expires_in": 14399,
    "scope": "cuckoo-service"
}

接下来就可以带着token去访问资源服务器的资源了

curl --location --request GET 'http://127.0.0.1:8000/goods?access_token=56526c6f-abcb-41c6-bb35-812a76e2a049'

总结

可以看到redistoken这里默认用的是jdk的序列化策略,spring也提供了1.0和2.0版本的jackson序列化策略,如下

这里折腾了很久,最后写了一个策略类,也就是被我注释掉的那行代码,最开始各种找序列化策略去重写,最后发现自己用jackson手动去实现serializeInternal是没问题的,但是,这里反序列化会有问题,因为OAuth2Authentication是没有无参构造方法的,所以jackson没法实现反序列化。

public class Oauth2JsonSerializationStrategy extends StandardStringSerializationStrategy {
    @Override
    protected <T> T deserializeInternal(byte[] bytes, Class<T> clazz) {
        return JsonUtils.parse(new String(bytes, StandardCharsets.UTF_8), clazz);
    }

    @Override
    protected byte[] serializeInternal(Object object) {
        return Objects.requireNonNull(JsonUtils.convert(object)).getBytes();
    }
}
@Slf4j
public class JsonUtils {
    private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
    
    public static <T> T parse(String json, Class<T> clazz) {
        try {
            return OBJECT_MAPPER.readValue(json, clazz);
        } catch (IOException e) {
            log.error("jackson 字符串转json失败:{}", e.getMessage());
        }
        return null;
    }

    public static String convert(Object data) {
        try {
            return OBJECT_MAPPER.writeValueAsString(data);
        } catch (JsonProcessingException e) {
            log.error("jackson json转字符串失败:{}", e.getMessage());
        }
        return null;
    }
}

我用fastjson也尝试过,也或多或少有些小问题,暂时采用默认的jdk序列化策略,折腾了两天时间也算跟了不少源码,都是自己琢磨出来的,还是有收获的。

网上看有人是重写序列化策略,这种方案应该是可行的,等后面找到更好的方案再更新本帖

以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。

相关文章

  • 在Spring项目中引入高版本依赖并解决低版本冲突问题的解决方法

    在Spring项目中引入高版本依赖并解决低版本冲突问题的解决方法

    在Spring项目的开发过程中,依赖管理是一个非常重要且复杂的问题,我们可能需要引入更高版本的依赖来使用新特性或修复旧版本的Bug,然而,这些高版本依赖可能会与项目中已有的低版本依赖产生冲突,本文将详细探讨如何在Spring中引入高版本依赖,并解决低版本依赖冲突的问题
    2025-03-03
  • java服务自动停止原因查找方式

    java服务自动停止原因查找方式

    这篇文章主要介绍了java服务自动停止原因查找方式,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教
    2023-12-12
  • MybatisX-Generator不生成domain文件夹的问题及解决

    MybatisX-Generator不生成domain文件夹的问题及解决

    在使用MybatisX-Generator生成数据库表实体时,如果发现没有生成domain文件夹以及User.java文件,是因为MybatisX版本更新,最新版需要在options里额外勾选model才能生成domain,勾选model并点击finish后,成功生成domain文件夹及User.java文件
    2025-01-01
  • java返回json请求中文变成问号的问题及解决

    java返回json请求中文变成问号的问题及解决

    这篇文章主要介绍了java返回json请求中文变成问号的问题及解决方案,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2022-07-07
  • Java程序包不存在的两种解决方法

    Java程序包不存在的两种解决方法

    有时候我们在导入程序之后,系统会给出错误提示:Java:程序包xxxx不存在,本文主要介绍了Java程序包不存在的两种解决方法,具有一定的参考价值,感兴趣的可以了解一下
    2024-02-02
  • request.getParameter()方法的简单理解与运用方式

    request.getParameter()方法的简单理解与运用方式

    在JavaWeb开发中,request对象扮演着至关重要的角色,它是HTTP请求的封装,request.getParameter()用于获取客户端通过GET或POST方式发送的参数,与之相对,request.setAttribute()用于在服务器端设置属性,这些属性只在一次请求中有效
    2024-10-10
  • 深入理解Java中的IOUtils(示例演示)

    深入理解Java中的IOUtils(示例演示)

    Java中的IOUtils是一个工具类,用于简化文件和流的操作,它提供了一些常用的方法,如复制文件、读取文件、写入文件等,这篇文章主要介绍了深入理解Java中的IOUtils(示例演示),需要的朋友可以参考下
    2023-08-08
  • 解决springcloud-gateway限流遇到的问题

    解决springcloud-gateway限流遇到的问题

    这篇文章主要介绍了解决springcloud-gateway限流遇到的问题,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2021-07-07
  • Spring Boot 消息队列与异步处理的应用小结

    Spring Boot 消息队列与异步处理的应用小结

    这篇文章给大家介绍了Spring Boot 消息队列与异步处理的应用小结,本文结合实例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友参考下吧
    2026-04-04
  • 15道非常经典的Java面试题 附详细答案

    15道非常经典的Java面试题 附详细答案

    这篇文章主要为大家推荐了15道非常经典的Java面试题,附详细答案,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2016-10-10

最新评论