Spring?security?oauth2以redis作為tokenstore及jackson序列化失敗問(wèn)題
前言
項(xiàng)目當(dāng)中需要用到鑒權(quán)的場(chǎng)景很多,一般會(huì)使用shiro或者spring security作為一個(gè)權(quán)限驗(yàn)證的框架,兩個(gè)框架的優(yōu)缺點(diǎn)這里就不比較了,都是看個(gè)人習(xí)慣。
自己從搭建項(xiàng)目時(shí)就比較傾向于選擇spring全家桶,所以就選擇了spring security + oauth2的模式,一開始是使用jwt(Java-web-token)的方式,沒(méi)別的,因?yàn)檩p,但是慢慢后續(xù)因?yàn)楣δ苌系男枨蟮?,出現(xiàn)了對(duì)token進(jìn)行管理的需求,這才開始啟用redis存儲(chǔ)token。
一、TokenStore
顧名思義就是存儲(chǔ)token和用來(lái)鑒權(quán)的倉(cāng)庫(kù),spring自己實(shí)現(xiàn)了四種方案
內(nèi)存存儲(chǔ),數(shù)據(jù)都是基于內(nèi)存的,項(xiàng)目重啟就沒(méi)了
jdbc存儲(chǔ),管理系統(tǒng)用的比較多,并發(fā)吞吐不高的情況下搓搓有余了,而且坑比較少
jwt,這也就是我之前用的,好處就是token可以攜帶需要的信息,避免二次查詢,記住不要存放敏感信息,而且RSA非對(duì)稱加密的安全性也夠了,缺點(diǎn)就是無(wú)法主動(dòng)失效
我們今天要看的redis存儲(chǔ),其實(shí)和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ù)注冊(cè)中心和配置中心了,這個(gè)不影響
- 授權(quán)服務(wù)器
<dependency> <!-- 指明版本,解決redis存儲(chǔ)出現(xiàn)的問(wèn)題:java.lang.NoSuchMethodError: org.springframework.data.redis.connection.RedisConnection.set([B[B)V問(wèn)題 --> <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小時(shí) 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的序列化策略,沒(méi)搞定 //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/** 端點(diǎn) 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校驗(yàn) http.headers().frameOptions().disable(); //登陸 驗(yàn)證碼 swagger接口及js文件 http.csrf().disable().authorizeRequests() .antMatchers("/actuator/**").permitAll() .antMatchers("/**").authenticated(); } @Override public void configure(ResourceServerSecurityConfigurer resources) throws Exception { //無(wú)狀態(tài) resources.stateless(true).tokenStore(tokenStore()); } /** * 設(shè)置token存儲(chǔ),這一點(diǎn)配置要與授權(quán)服務(wù)器相一致 */ @Bean public RedisTokenStore tokenStore() { RedisTokenStore redisTokenStore = new RedisTokenStore(redisConnectionFactory); //自定義了jackson的序列化策略,沒(méi)搞定 //redisTokenStore.setSerializationStrategy(new Oauth2JsonSerializationStrategy()); //redis key前綴 redisTokenStore.setPrefix("karl-auth-token:"); return redisTokenStore; }
2.測(cè)試
我這邊用了mysql存儲(chǔ)client信息,配置了密碼和授權(quán)碼的模式,這里用密碼的方式測(cè)試
請(qǐng)求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" }
接下來(lái)就可以帶著token去訪問(wèn)資源服務(wù)器的資源了
curl --location --request GET 'http://127.0.0.1:8000/goods?access_token=56526c6f-abcb-41c6-bb35-812a76e2a049'
總結(jié)
可以看到redistoken這里默認(rèn)用的是jdk的序列化策略,spring也提供了1.0和2.0版本的jackson序列化策略,如下
這里折騰了很久,最后寫了一個(gè)策略類,也就是被我注釋掉的那行代碼,最開始各種找序列化策略去重寫,最后發(fā)現(xiàn)自己用jackson手動(dòng)去實(shí)現(xiàn)serializeInternal
是沒(méi)問(wèn)題的,但是,這里反序列化會(huì)有問(wèn)題,因?yàn)?code>OAuth2Authentication是沒(méi)有無(wú)參構(gòu)造方法的,所以jackson沒(méi)法實(shí)現(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也嘗試過(guò),也或多或少有些小問(wèn)題,暫時(shí)采用默認(rèn)的jdk序列化策略,折騰了兩天時(shí)間也算跟了不少源碼,都是自己琢磨出來(lái)的,還是有收獲的。
網(wǎng)上看有人是重寫序列化策略,這種方案應(yīng)該是可行的,等后面找到更好的方案再更新本帖
以上為個(gè)人經(jīng)驗(yàn),希望能給大家一個(gè)參考,也希望大家多多支持腳本之家。
相關(guān)文章
MyBatis-Plus解決邏輯刪除與唯一索引的問(wèn)題
本文主要介紹了MyBatis-Plus解決邏輯刪除與唯一索引的問(wèn)題,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2022-08-08Java編程實(shí)現(xiàn)從尾到頭打印鏈表代碼實(shí)例
這篇文章主要介紹了Java編程實(shí)現(xiàn)從尾到頭打印鏈表代碼實(shí)例,小編覺(jué)得挺不錯(cuò)的,這里分享給大家,供需要的朋友參考。2017-10-10Java結(jié)合redistemplate使用分布式鎖案例講解
在Java中使用RedisTemplate結(jié)合Redis來(lái)實(shí)現(xiàn)分布式鎖是一種常見(jiàn)的做法,特別適用于微服務(wù)架構(gòu)或多實(shí)例部署的應(yīng)用程序中,以確保數(shù)據(jù)的一致性和避免競(jìng)態(tài)條件,下面給大家分享使用Spring Boot和RedisTemplate實(shí)現(xiàn)分布式鎖的案例,感興趣的朋友一起看看吧2024-08-08Java8中AbstractExecutorService與FutureTask源碼詳解
這篇文章主要給大家介紹了關(guān)于Java8中AbstractExecutorService與FutureTask的相關(guān)資料,文中通過(guò)實(shí)例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2022-01-01SpringBoot文件上傳(本地存儲(chǔ))回顯前端操作方法
這篇文章主要介紹了SpringBoot文件上傳(本地存儲(chǔ))回顯前端操作方法的相關(guān)資料,文中講解了文件上傳的基本原理,包括前端調(diào)用后端接口上傳文件,后端返回文件路徑給前端,前端通過(guò)路徑訪問(wèn)圖片,需要的朋友可以參考下2024-11-11springboot+thymeleaf+druid+mybatis 多模塊實(shí)現(xiàn)用戶登錄功能
這篇文章主要介紹了springboot+thymeleaf+druid+mybatis 多模塊實(shí)現(xiàn)用戶登錄功能,本文通過(guò)示例代碼圖文相結(jié)合給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2020-07-07