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数据,这里就结束了。