Spring?security?oauth2以redis作為tokenstore及jackson序列化失敗問題
前言
項目當中需要用到鑒權(quán)的場景很多,一般會使用shiro或者spring security作為一個權(quán)限驗證的框架,兩個框架的優(yōu)缺點這里就不比較了,都是看個人習慣。
自己從搭建項目時就比較傾向于選擇spring全家桶,所以就選擇了spring security + oauth2的模式,一開始是使用jwt(Java-web-token)的方式,沒別的,因為輕,但是慢慢后續(xù)因為功能上的需求迭代,出現(xiàn)了對token進行管理的需求,這才開始啟用redis存儲token。
一、TokenStore
顧名思義就是存儲token和用來鑒權(quán)的倉庫,spring自己實現(xiàn)了四種方案

內(nèi)存存儲,數(shù)據(jù)都是基于內(nèi)存的,項目重啟就沒了
jdbc存儲,管理系統(tǒng)用的比較多,并發(fā)吞吐不高的情況下搓搓有余了,而且坑比較少
jwt,這也就是我之前用的,好處就是token可以攜帶需要的信息,避免二次查詢,記住不要存放敏感信息,而且RSA非對稱加密的安全性也夠了,缺點就是無法主動失效
我們今天要看的redis存儲,其實和jdbc一樣,區(qū)別在于,我速度快,哈哈哈哈
二、步驟
1.配置和代碼
1.1環(huán)境
- spring boot 2.0.9.RELEASE
- redis 5.0.6 集群
- mysql 8.0 + druid連接池 + mybatis
我這里用了spring cloud alibaba,nacos作為服務(wù)注冊中心和配置中心了,這個不影響
- 授權(quán)服務(wù)器
<dependency>
<!-- 指明版本,解決redis存儲出現(xiàn)的問題: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>
- 資源服務(wù)器
<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 授權(quán)服務(wù)器配置文件
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 資源服務(wù)器配置文件
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 授權(quán)服務(wù)器代碼
首先是授權(quán)服務(wù)器的配置
@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參數(shù)
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 被調(diào)用
}
@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 資源服務(wù)器代碼
@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 {
//關(guān)閉iframe校驗
http.headers().frameOptions().disable();
//登陸 驗證碼 swagger接口及js文件
http.csrf().disable().authorizeRequests()
.antMatchers("/actuator/**").permitAll()
.antMatchers("/**").authenticated();
}
@Override
public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
//無狀態(tài)
resources.stateless(true).tokenStore(tokenStore());
}
/**
* 設(shè)置token存儲,這一點配置要與授權(quán)服務(wù)器相一致
*/
@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信息,配置了密碼和授權(quán)碼的模式,這里用密碼的方式測試
請求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=='
獲取到的結(jié)果是
{
"access_token": "56526c6f-abcb-41c6-bb35-812a76e2a049",
"token_type": "bearer",
"refresh_token": "ac2e0962-e806-4549-af67-18edc1990d5a",
"expires_in": 14399,
"scope": "cuckoo-service"
}
接下來就可以帶著token去訪問資源服務(wù)器的資源了
curl --location --request GET 'http://127.0.0.1:8000/goods?access_token=56526c6f-abcb-41c6-bb35-812a76e2a049'
總結(jié)

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

這里折騰了很久,最后寫了一個策略類,也就是被我注釋掉的那行代碼,最開始各種找序列化策略去重寫,最后發(fā)現(xiàn)自己用jackson手動去實現(xiàn)serializeInternal是沒問題的,但是,這里反序列化會有問題,因為OAuth2Authentication是沒有無參構(gòu)造方法的,所以jackson沒法實現(xiàn)反序列化。
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 字符串轉(zhuǎn)json失?。簕}", e.getMessage());
}
return null;
}
public static String convert(Object data) {
try {
return OBJECT_MAPPER.writeValueAsString(data);
} catch (JsonProcessingException e) {
log.error("jackson json轉(zhuǎn)字符串失敗:{}", e.getMessage());
}
return null;
}
}
我用fastjson也嘗試過,也或多或少有些小問題,暫時采用默認的jdk序列化策略,折騰了兩天時間也算跟了不少源碼,都是自己琢磨出來的,還是有收獲的。
網(wǎng)上看有人是重寫序列化策略,這種方案應(yīng)該是可行的,等后面找到更好的方案再更新本帖
以上為個人經(jīng)驗,希望能給大家一個參考,也希望大家多多支持腳本之家。
相關(guān)文章
Java結(jié)合redistemplate使用分布式鎖案例講解
在Java中使用RedisTemplate結(jié)合Redis來實現(xiàn)分布式鎖是一種常見的做法,特別適用于微服務(wù)架構(gòu)或多實例部署的應(yīng)用程序中,以確保數(shù)據(jù)的一致性和避免競態(tài)條件,下面給大家分享使用Spring Boot和RedisTemplate實現(xiàn)分布式鎖的案例,感興趣的朋友一起看看吧2024-08-08
Java8中AbstractExecutorService與FutureTask源碼詳解
這篇文章主要給大家介紹了關(guān)于Java8中AbstractExecutorService與FutureTask的相關(guān)資料,文中通過實例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友可以參考下2022-01-01
springboot+thymeleaf+druid+mybatis 多模塊實現(xiàn)用戶登錄功能
這篇文章主要介紹了springboot+thymeleaf+druid+mybatis 多模塊實現(xiàn)用戶登錄功能,本文通過示例代碼圖文相結(jié)合給大家介紹的非常詳細,對大家的學習或工作具有一定的參考借鑒價值,需要的朋友可以參考下2020-07-07

