Spring Security OAuth2使用Redis作為token存儲
授權application.yml 服務器保存token到Redis
server: port: 8080 spring: redis: host: 127.0.0.1 port: 6379 password: 123 database: 0 #也可以在代碼中指定
Maven依賴
<dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2.62</version> </dependency>
在spring security oauth2中,授權服務使用redis存儲token的時候,報錯:
java.lang.NoSuchMethodError: org.springframework.data.redis.connection.RedisConnection.set([B[B)V
這說明版本有問題,解決方案是,將oauth2的版本升級到2.3.3,即在pom文件中,加入:
<!-- oauth2 start --> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-oauth2</artifactId> </dependency> <dependency> <groupId>org.springframework.security.oauth</groupId> <artifactId>spring-security-oauth2</artifactId> <!-- 指明版本,解決redis存儲出現的問題:java.lang.NoSuchMethodError: org.springframework.data.redis.connection.RedisConnection.set([B[B)V問題 --> <version>2.3.3.RELEASE</version> </dependency> <!-- oauth2 end -->
代碼分為認證端和客戶端兩個服務
認證端,也就是我的security服務
有兩個文件,一個配置問津,一個
1.AuthResourcesConfig
package com.adao.security.config;
import com.adao.security.common.AuthResourcesConfig;
import com.adao.security.service.SSOUserDetailsService;
import lombok.extern.log4j.Log4j2;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer;
import org.springframework.security.oauth2.provider.token.DefaultTokenServices;
import org.springframework.security.oauth2.provider.token.store.redis.RedisTokenStore;
/** * @author * @version 1.0 * @date 2021/8/12 * @Description: 資源服務信息公共配置類 */ public class AuthResourcesConfig { /** * token過期時間,單位為秒 */ public static final int TOKEN_SECONDS_ACCESS = 10 * 60; /** * 刷新token時間,單位為秒 */ public static final int TOKEN_SECONDS_REFRESH = 10 * 60; /** * 資源服務客戶端ID信息 */ //rtp public static final String CLIENT_RTP = "RTP"; //manage public static final String CLIENT_RTP_MANAGE = "adao-rtp-manage"; /** * 資源節點對應的密鑰,目前統一為 adao */ public static final String CLIENT_RTP_SECRET = "adao"; }
2.AuthorizationServerConfig
package com.adao.security.config; import com.adao.security.common.AuthResourcesConfig; import com.adao.security.service.SSOUserDetailsService; import lombok.extern.log4j.Log4j2; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.redis.connection.RedisConnectionFactory; import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer; import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter; import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer; import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer; import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer; import org.springframework.security.oauth2.provider.token.DefaultTokenServices; import org.springframework.security.oauth2.provider.token.store.redis.RedisTokenStore; /** * @author adao * @version 1.0 * @Description: 認證服務器配置 * @date 2021/8/11 */ @Configuration @EnableAuthorizationServer @Log4j2 public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter { /** * 鑒權模式 */ public static final String GRANT_TYPE[] = {"password", "refresh_token"}; @Autowired private AuthenticationManager authenticationManager; /** * 用戶服務 */ @Autowired public SSOUserDetailsService userDetailsService; @Autowired private RedisConnectionFactory redisConnectionFactory; @Autowired private StringRedisTemplate redisTemplate; /** * Redis數據庫存 */ public static final int REDIS_CONNECTION_DATABASE = 14; /** * 客戶端信息配置,可配置多個客戶端,可以使用配置文件進行代替 * * @param clients 客戶端設置 * @throws Exception 異常 */ @Override public void configure(ClientDetailsServiceConfigurer clients) throws Exception { // 定義客戶端應用的通行證 clients.inMemory() // rtp-manage .withClient(AuthResourcesConfig.CLIENT_RTP_MANAGE) .secret(new BCryptPasswordEncoder().encode(AuthResourcesConfig.CLIENT_RTP_SECRET)) .authorizedGrantTypes(GRANT_TYPE[0], GRANT_TYPE[1]) .scopes("all") .autoApprove(true) // rtp .and() .withClient(AuthResourcesConfig.CLIENT_RTP) .secret(new BCryptPasswordEncoder().encode(AuthResourcesConfig.CLIENT_RTP_SECRET)) .authorizedGrantTypes(GRANT_TYPE[0], GRANT_TYPE[1]) .scopes("all") .autoApprove(true); } /** * 配置端點 * * @param endpoints 端點 * @throws Exception 異常 */ @Override public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception { //配置認證管理器 endpoints.authenticationManager(authenticationManager) //配置用戶服務 .userDetailsService(userDetailsService) //配置token存儲的服務與位置 .tokenServices(tokenService()) .tokenStore(redisTokenStore()); } @Override public void configure(AuthorizationServerSecurityConfigurer security) { //允許用戶訪問OAuth2 授權接口 security.allowFormAuthenticationForClients(); //允許已授權用戶訪問 checkToken 接口和獲取 token 接口 security.tokenKeyAccess("permitAll()"); //允許已授權用戶獲取 token 接口 security.checkTokenAccess("permitAll()"); } /** * 設置token存儲,資源服務器配置與此處相一致 */ @Bean public RedisTokenStore redisTokenStore() { // 指定redis數據庫存儲token,與業務庫區分。 LettuceConnectionFactory lettuceConnectionFactory = (LettuceConnectionFactory) redisTemplate.getConnectionFactory(); lettuceConnectionFactory.setDatabase(REDIS_CONNECTION_DATABASE); RedisTokenStore redisTokenStore = new RedisTokenStore(lettuceConnectionFactory);
// 也可以用在何種方式,這樣用哪個數據庫是在配置文件中指定
//RedisTokenStore redisTokenStore = new RedisTokenStore(redisConnectionFactory);
log.info("Oauth2 redis database : [{}]", lettuceConnectionFactory.getDatabase()); //設置redis token存儲中的前綴 redisTokenStore.setPrefix("auth-token:"); return redisTokenStore; } @Bean public DefaultTokenServices tokenService() { DefaultTokenServices tokenServices = new DefaultTokenServices(); //配置token存儲 tokenServices.setTokenStore(redisTokenStore()); //開啟支持refresh_token tokenServices.setSupportRefreshToken(true); //復用refresh_token tokenServices.setReuseRefreshToken(true); //token有效期 tokenServices.setAccessTokenValiditySeconds(AuthResourcesConfig.TOKEN_SECONDS_ACCESS); //refresh_token有效期 tokenServices.setRefreshTokenValiditySeconds(AuthResourcesConfig.TOKEN_SECONDS_REFRESH); return tokenServices; } }
接下來是資源端
1.ResourceServerConfig
package com.adao.manage.config; import com.adao.manage.common.AuthExceptionHandler; import lombok.extern.log4j.Log4j2; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.redis.connection.RedisConnectionFactory; import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer; import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter; import org.springframework.security.oauth2.config.annotation.web.configurers.ResourceServerSecurityConfigurer; import org.springframework.security.oauth2.provider.token.store.redis.RedisTokenStore; /** * @author adao * @version 1.0 * @date 2021/8/11 * @description 資源服務配置類 */ @Configuration @EnableResourceServer @EnableGlobalMethodSecurity(prePostEnabled = true) @Log4j2 public class ResourceServerConfig extends ResourceServerConfigurerAdapter { @Autowired private RedisConnectionFactory redisConnectionFactory; @Autowired private AuthExceptionHandler authExceptionHandler; @Autowired private StringRedisTemplate redisTemplate; /** * Redis數據庫存 */ public static final int REDIS_CONNECTION_DATABASE = 14; @Override public void configure(ResourceServerSecurityConfigurer resources) throws Exception { //狀態 resources.stateless(true) .accessDeniedHandler(authExceptionHandler) .authenticationEntryPoint(authExceptionHandler); //設置token存儲 resources.tokenStore(redisTokenStore()); } /** * 設置token存儲,這一點配置要與授權服務器相一致 */ @Bean public RedisTokenStore redisTokenStore() { // 指定redis數據庫存儲token,與業務庫區分。 LettuceConnectionFactory lettuceConnectionFactory = (LettuceConnectionFactory) redisTemplate.getConnectionFactory(); lettuceConnectionFactory.setDatabase(REDIS_CONNECTION_DATABASE); RedisTokenStore redisTokenStore = new RedisTokenStore(lettuceConnectionFactory); log.info("Oauth2 redis database : [{}]",lettuceConnectionFactory.getDatabase()); //設置redis token存儲中的前綴 redisTokenStore.setPrefix("auth-token:"); return redisTokenStore; } @Override public void configure(HttpSecurity http) throws Exception { //請求權限配置 http.authorizeRequests() //下邊的路徑放行,不需要經過認證 .antMatchers("/actuator/health").permitAll() .antMatchers("/v2/api-docs", "/swagger-resources/configuration/ui", "/swagger-resources", "/swagger-resources/configuration/security", "/swagger-ui.html", "/webjars/**").permitAll() //其余接口沒有角色限制,但需要經過認證,只要攜帶token就可以放行 .antMatchers("/dictionary/**").permitAll() .anyRequest() .authenticated(); } }
AuthExceptionHandler 異常捕獲處理類
package com.adao.manage.common; import com.alibaba.fastjson.JSON; import lombok.extern.slf4j.Slf4j; import org.springframework.security.access.AccessDeniedException; import org.springframework.security.core.AuthenticationException; import org.springframework.security.oauth2.common.exceptions.InvalidTokenException; import org.springframework.security.web.AuthenticationEntryPoint; import org.springframework.security.web.access.AccessDeniedHandler; import org.springframework.stereotype.Component; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; /** * @author adao * @version 1.0 * @date 2021/8/11 * @Description: 權限不足返回信息處理類 */ @Component @Slf4j public class AuthExceptionHandler implements AuthenticationEntryPoint, AccessDeniedHandler { @Override public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException { Throwable cause = authException.getCause(); response.setContentType("application/json;charset=UTF-8"); response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); // CORS "pre-flight" request response.addHeader("Access-Control-Allow-Origin", "*"); response.addHeader("Cache-Control", "no-cache"); response.addHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS"); response.setHeader("Access-Control-Allow-Headers", "x-requested-with"); response.addHeader("Access-Control-Max-Age", "1800"); if (cause instanceof InvalidTokenException) { log.error("InvalidTokenException : {}", cause.getMessage()); //Token無效 response.getWriter().write(JSON.toJSONString(ApiResult.fail(ApiCode.ACCESS_TOKEN_INVALID))); } else { log.error("AuthenticationException : NoAuthentication"); //資源未授權 response.getWriter().write(JSON.toJSONString(ApiResult.fail(ApiCode.ACCESS_UNAUTHORIZED))); } } @Override public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException { response.setContentType("application/json;charset=UTF-8"); response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); response.addHeader("Access-Control-Allow-Origin", "*"); response.addHeader("Cache-Control", "no-cache"); response.addHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS"); response.setHeader("Access-Control-Allow-Headers", "x-requested-with"); response.addHeader("Access-Control-Max-Age", "1800"); //訪問資源的用戶權限不足 log.error("AccessDeniedException : {}", accessDeniedException.getMessage()); response.getWriter().write(JSON.toJSONString(ApiResult.fail(ApiCode.INSUFFICIENT_PERMISSIONS))); } }
公用的范圍code及 含義
package com.adao.manage.common; public enum ApiCode { SUCCESS(200, "操作成功"), /** * 表示接口調用方異常提示 */ ACCESS_TOKEN_INVALID(1001, "access_token 無效"), REFRESH_TOKEN_INVALID(1002, "refresh_token 無效"), INSUFFICIENT_PERMISSIONS(1003, "該用戶權限不足以訪問該資源接口"), ACCESS_UNAUTHORIZED(1004, "訪問此資源需要身份驗證"), }
最后開始測試
postman 設置
http://192.168.10.90:6005/oauth/token?grant_type=password&username=zq&password=123456&scope=all
直接結果可以看到,獲取到了token數據,這里就結束了。