Spring Security OAuth2 单点登录和登出

article/2025/8/20 16:44:33

文章目录

    • 1. 单点登录
      • 1.1 使用内存保存客户端和用户信息
        • 1.1.1 认证中心 auth-server
        • 1.1.2 子系统 service-1
        • 1.1.3 测试
      • 1.2 使用数据库保存客户端和用户信息
      • 1.3 单点登录流程
        • 1.2.1 请求授权码,判断未登录,重定向登录页
        • 1.2.2 登录成功,重定向继续请求授权码,未被资源所有者批准,返回批准页面
        • 1.2.3 资源所有者批准,重定向返回授权码
        • 1.2.4 客户端获取到授权码,请求Token
        • 1.2.5 获取到Token,重定向 /user
      • 1.3 JWT Token
        • 1.3.1 资源服务器未添加tokenServices
        • 1.3.2 资源服务器添加tokenServices
    • 2. 单点登出
    • 3. 总结

Spring Security OAuth 最新官方已经不再维护,以下内容只用于学习记录。

GitHub:shpunishment/spring-security-oauth2-demo

1. 单点登录

单点登录即有多个子系统,有一个认证中心。当访问其中任意一个子系统时,如果发现未登录,就跳到认证中心进行登录,登录完成后再跳回该子系统。此时访问其他子系统时,就已经是登录状态了。登出统一从认证中心登出,登出后各个子系统就无法访问了,需要再次登录。

Spring Security OAuth 建立在Spring Security 之上,所以大部分配置还是在Security中,Security完成对用户的认证和授权,OAuth完成单点登录。

Spring Security OAuth 的单点登录主要靠@EnableOAuth2Sso实现,简化了从资源服务器到认证授权服务器的SSO流程,并使用授权码方式获取。

1.1 使用内存保存客户端和用户信息

1.1.1 认证中心 auth-server

添加依赖

<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency><groupId>org.springframework.security.oauth</groupId><artifactId>spring-security-oauth2</artifactId><version>2.3.8.RELEASE</version>
</dependency>
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!--spring2.0集成redis所需common-pool2-->
<dependency><groupId>org.apache.commons</groupId><artifactId>commons-pool2</artifactId><version>2.4.2</version>
</dependency>
<dependency><groupId>com.alibaba</groupId><artifactId>fastjson</artifactId><version>1.2.60</version>
</dependency>

application.yml

server:port: 8000servlet:context-path: /auth-serversession:cookie:name: oauth-auth-serverspring:redis:# Redis默认情况下有16个分片,这里配置具体使用的分片,默认是0database: 0host: localhostport: 6379# 连接密码(默认为空)password:# 连接超时时间(毫秒)timeout: 10000mslettuce:pool:# 连接池最大连接数(使用负值表示没有限制) 默认 8max-active: 8# 连接池最大阻塞等待时间(使用负值表示没有限制) 默认 -1max-wait: -1# 连接池中的最大空闲连接 默认 8max-idle: 8# 连接池中的最小空闲连接 默认 0min-idle: 0

添加授权服务器配置,主要令牌路径的安全性,客户端详情和令牌存储。

这里配置了一个客户端,支持授权码模式和刷新Token,并且将Token存在Redis中。

@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {private static final String RESOURCE_ID = "resource-1";@Autowiredprivate PasswordEncoder passwordEncoder;@Autowiredprivate RedisConnectionFactory redisConnectionFactory;/*** 配置授权服务器的安全性,令牌端点的安全约束** @param security* @throws Exception*/@Overridepublic void configure(AuthorizationServerSecurityConfigurer security) throws Exception {security// 开启 /oauth/check_token.tokenKeyAccess("permitAll()")// 开启 /oauth/token_key.checkTokenAccess("isAuthenticated()")// 允许表单认证// 如果配置,且url中有client_id和client_secret的,则走 ClientCredentialsTokenEndpointFilter// 如果没有配置,但是url中没有client_id和client_secret的,走basic认证保护.allowFormAuthenticationForClients();}/*** 配置客户端,可存在内存和数据库中** @param clients* @throws Exception*/@Overridepublic void configure(ClientDetailsServiceConfigurer clients) throws Exception {clients.inMemory().withClient("client_1").resourceIds(RESOURCE_ID).authorizedGrantTypes("authorization_code", "refresh_token").scopes("read").authorities("client").secret(passwordEncoder.encode("123456"))// 必须添加,会和请求时重定向地址匹配.redirectUris("http://localhost:8001/service1/login")// 自动批准,在登录成功后不会跳到批准页面,让资源所有者批准//.autoApprove(true);}/**** 配置授权服务器端点的非安全功能,例如令牌存储,令牌自定义,用户批准和授予类型** @param endpoints* @throws Exception*/@Overridepublic void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {endpoints// 令牌存在redis.tokenStore(tokenStore());}/*** 配置redis,使用redis存token* @return*/@Beanpublic TokenStore tokenStore(){return new RedisTokenStore(redisConnectionFactory);}
}

添加资源服务器配置,主要配置资源id和需要Token验证的url

对于相同的url,如果二者都配置了验证,则优先进入ResourceServerConfigurerAdapter,会被 OAuth2AuthenticationProcessingFilter 处理,进行token验证;而不会进行WebSecurityConfigurerAdapter 的表单认证等。

@Configuration
@EnableResourceServer
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {private static final String RESOURCE_ID = "resource-1";/*** 添加特定于资源服务器的属性** @param resources*/@Overridepublic void configure(ResourceServerSecurityConfigurer resources) throws Exception {resources.resourceId(RESOURCE_ID);}/*** 使用此配置安全资源的访问规则,配置需要token验证的url。 默认情况下,所有不在"/oauth/**"中的资源都受到保护。** @param http* @throws Exception*/@Overridepublic void configure(HttpSecurity http) throws Exception {// 只有 /security/getUserInfo 需要token验证http.requestMatchers().antMatchers("/security/getUserInfo").and().authorizeRequests().anyRequest().authenticated();}
} 

security配置,用户数据,自定义登录页,成功失败Handler,session,配置非受保护URL等。

这里添加了两个用户以及登录页等配置。

@Configuration
public class ServerWebSecurityConfig extends WebSecurityConfigurerAdapter {/*** 认证管理器配置,用于信息获取来源(UserDetails)以及密码校验规则(PasswordEncoder)** @param auth* @throws Exception*/@Overrideprotected void configure(AuthenticationManagerBuilder auth) throws Exception {auth// 使用内存认证,在内存中保存两个用户.inMemoryAuthentication().passwordEncoder(passwordEncoder())// admin 拥有ADMIN和USER的权限.withUser("admin").password(passwordEncoder().encode("admin")).roles("ADMIN", "USER").and()// user 拥有USER的权限.withUser("user").password(passwordEncoder().encode("user")).roles("USER");}/*** 核心过滤器配置,更多使用ignoring()用来忽略对静态资源的控制* @param web* @throws Exception*/@Overridepublic void configure(WebSecurity web) throws Exception {web.ignoring().antMatchers("/static/js/**");}/*** 安全过滤器链配置,自定义安全访问策略* @param http* @throws Exception*/@Overrideprotected void configure(HttpSecurity http) throws Exception {http.authorizeRequests()// /login 和 /oauth/authorize 路径配置为不需要任何身份验证,其他所有路径必须经过验证.antMatchers("/login", "/oauth/authorize").permitAll()// 其他请求都需要已认证.anyRequest().authenticated().and()// 使用表单登录.formLogin()// 自定义username 和password参数.usernameParameter("login_username").passwordParameter("login_password")// 自定义登录页地址.loginPage("/loginPage")// 验证表单的地址,由过滤器 UsernamePasswordAuthenticationFilter 拦截处理.loginProcessingUrl("/login").permitAll().and().csrf().disable();}@Beanpublic static BCryptPasswordEncoder passwordEncoder() {return new BCryptPasswordEncoder();}
}

获取当前用户信息,供客户端获取

@RestController
@RequestMapping("/security")
public class SecurityController {@GetMapping("/getUserInfo")@ResponseBodypublic Principal getUserInfo(Principal principal) {return principal;}
}

auth-server

1.1.2 子系统 service-1

添加依赖

<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId>
</dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency><dependency><groupId>org.springframework.security.oauth.boot</groupId><artifactId>spring-security-oauth2-autoconfigure</artifactId><version>2.1.13.RELEASE</version>
</dependency>

application.yml

server:port: 8001servlet:context-path: /service1session:cookie:name: oauth-service-1security:oauth2:client:clientId: client_1clientSecret: 123456# 获取访问令牌的URIaccessTokenUri: http://localhost:8000/auth-server/oauth/token# 将用户重定向到的授权URIuserAuthorizationUri: http://localhost:8000/auth-server/oauth/authorizeresource:# 获取当前用户详细信息userInfoUri: http://localhost:8000/auth-server/security/getUserInfo

security配置,如果需要对service-1的url进行控制,需要添加 WebSecurityConfigurerAdapter 配置,可配置子系统中哪些接口需要auth-server的认证,配置非受保护URL等。

@Configuration
// @EnableOAuth2Sso 注解 在继承 WebSecurityConfigurerAdapter 类的上面时
// 代表着在该子类配置的基础上增强 OAuth2Sso 相关配置。
@EnableOAuth2Sso
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class ClientWebSecurityConfig extends WebSecurityConfigurerAdapter {/*** 安全过滤器链配置,自定义安全访问策略。可配置客户端不受保护的资源** @param http* @throws Exception*/@Overridepublic void configure(HttpSecurity http) throws Exception {http.antMatcher("/**").authorizeRequests()// 访问 / /home 不用认证.antMatchers("/", "/home").permitAll().anyRequest().authenticated().and()// 权限不足跳转 /401.exceptionHandling().accessDeniedPage("/401");}/*** 核心过滤器配置,更多使用ignoring()用来忽略对静态资源的控制和过滤微服务间feign的接口** @param web* @throws Exception*/@Overridepublic void configure(WebSecurity web) throws Exception {web.ignoring().antMatchers("/js/**");}
}

客户端资源服务器配置,只有 /api/* 需要token验证

@Configuration
@EnableResourceServer
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {private static final String RESOURCE_ID = "resource-1";/*** 添加特定于资源服务器的属性** @param resources*/@Overridepublic void configure(ResourceServerSecurityConfigurer resources) throws Exception {resources.resourceId(RESOURCE_ID);}/*** 使用此配置安全资源的访问规则,配置需要token验证的url。 默认情况下,所有不在"/oauth/**"中的资源都受到保护。** @param http* @throws Exception*/@Overridepublic void configure(HttpSecurity http) throws Exception {// /api/* 都需要token验证,会被 OAuth2AuthenticationProcessingFilter 处理http.requestMatchers().antMatchers("/api/*").and().authorizeRequests().anyRequest().authenticated();}
}

service1控制器

@Controller
public class Service1Controller {@RequestMapping(path = {"/", "/home"})public ModelAndView home() {return new ModelAndView("home");}@PreAuthorize("hasRole('USER')")@RequestMapping("/user")public ModelAndView user() {return new ModelAndView("user");}@PreAuthorize("hasRole('ADMIN')")@RequestMapping("/admin")public ModelAndView admin() {return new ModelAndView("admin");}/*** 测试 /api/* 是否被资源服务器拦截,需要token* @return*/@GetMapping("/api/getUserInfo")@ResponseBodypublic Principal getUserInfo() {return SecurityContextHolder.getContext().getAuthentication();}@GetMapping("/api2/getUserInfo")@ResponseBodypublic Principal getUserInfo2() {return SecurityContextHolder.getContext().getAuthentication();}
}

service-1

1.1.3 测试

service-2根据service-1复制一遍。

service-1和service-2不用登录即可访问 / /home
service-1 home
service-2 /
访问 /user 需要认证的资源,会先到auth-server进行认证
auth-server login
资源所有者批准
auth-server 批准
批准后才能访问到 /user
service-1 user
service-2的 /user 也可访问,即实现了单点登录
service-1 user
访问 /admin 用户权限不足
service-1 admin

1.2 使用数据库保存客户端和用户信息

只需要修改auth-server中客户端和用户信息的获取方式。

用户信息部分,修改security配置,参考 Spring Security 使用 中的使用数据库保存用户信息。

由于将Token等信息存在了Redis中,所以在数据库中只需要保存客户端信息。修改 AuthorizationServerConfig

@Autowired
private DataSource dataSource;@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {clients.withClientDetails(clientDetails(dataSource));
}/*** 获取客户端详细信息服务,JDBC实现* @return*/
@Bean
public ClientDetailsService clientDetails(DataSource dataSource) {return new JdbcClientDetailsService(dataSource);
}

添加表和数据,密码使用BCrypt加密,数据和使用内存时一致。

CREATE TABLE `oauth_client_details`  (`client_id` varchar(128) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,`resource_ids` varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,`client_secret` varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,`scope` varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,`authorized_grant_types` varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,`web_server_redirect_uri` varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,`authorities` varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,`access_token_validity` int(11) NULL DEFAULT NULL,`refresh_token_validity` int(11) NULL DEFAULT NULL,`additional_information` varchar(4096) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,`autoapprove` varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,PRIMARY KEY (`client_id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Compact;INSERT INTO `oauth_client_details` VALUES ('client_1', 'resource-1', '$2a$10$TfM5Bisse4ewbmIDfqZcxuYl5dI39/lEzzvkzlxFELKglHQM78FIu', 'read', 'authorization_code,refresh_token', 'http://localhost:8001/service1/login,http://localhost:8002/service2/login', NULL, NULL, NULL, NULL, NULL);

auth-server 数据库
效果与使用内存时一致。

1.3 单点登录流程

打开F12会看到以下重定向过程,可看到大致步骤:

  1. 请求授权码,判断未登录,重定向登录页
  2. 登录成功,重定向继续请求授权码,未被资源所有者批准,返回批准页面
  3. 资源所有者批准,重定向返回授权码
  4. 客户端获取到授权码,请求Token
  5. 获取到Token,重定向 /user

重定向过程

1.2.1 请求授权码,判断未登录,重定向登录页

访问客户端受保护资源 localhost:8001/service1/user,未登录重定向到 localhost:8001/service1/login 进行登录认证,因为配置了单点登录@EnableOAuth2Sso,所以单点登录拦截器会读取授权服务器的配置,发起获取授权码请求
http://localhost:8000/auth-server/oauth/authorize?client_id=client_1&redirect_uri=http://localhost:8001/service1/login&response_type=code&state=eEoQJJ

被auth-server的 AuthorizationEndpoint.authorize() 处理,因为未登录认证,抛出异常

if (!(principal instanceof Authentication) || !((Authentication) principal).isAuthenticated()) {throw new InsufficientAuthenticationException("User must be authenticated with Spring Security before authorization can be completed.");
}

异常在 ExceptionTranslationFilter.doFilter() 中处理

handleSpringSecurityException(request, response, chain, ase);

调用 LoginUrlAuthenticationEntryPoint.commence() 方法,获取登录页地址,并重定向

redirectUrl = buildRedirectUrlToLoginPage(request, response, authException);

1.2.2 登录成功,重定向继续请求授权码,未被资源所有者批准,返回批准页面

在auth-server中用户密码由 AbstractAuthenticationProcessingFilter.doFilter() 处理,UsernamePasswordAuthenticationFilter 继承自 AbstractAuthenticationProcessingFilter,在父类 doFilter() 方法中,会调用子类实现的 attemptAuthentication 方法,获取认证信息

authResult = attemptAuthentication(request, response);

在 attemptAuthentication() 方法中,将用户名和密码封装成token并认证,并添加额外信息后,进行认证

this.getAuthenticationManager().authenticate(authRequest);

getAuthenticationManager() 方法获取 AuthenticationManager 的实现类 ProviderManager,在 authenticate() 方法中,找到合适的 AuthenticationProvider 处理认证,这里是 DaoAuthenticationProvider,它父类 AbstractUserDetailsAuthenticationProvider 实现了该方法

result = provider.authenticate(authentication);

父类会调用 retrieveUser() 方法检索用户,实现在 DaoAuthenticationProvider

user = retrieveUser(username,(UsernamePasswordAuthenticationToken) authentication);

这里是从内存或数据库中获取用户,然后进行密码校验,成功后,将信息保存到Authentication,并返回。调用成功Handler,记住我等等。

默认登录成功,会重定向之前请求的地址
http://localhost:8000/auth-server/oauth/authorize?client_id=client_1&redirect_uri=http://localhost:8001/service1/login&response_type=code&state=eEoQJJ

再次被auth-server的 AuthorizationEndpoint.authorize() 处理,这时有用户认证信息,获取client信息,进行检查,检查资源所有者是否批准(客户端可设置是否自动批准)

如果未批准,返回批准页,请求转发 forward:/oauth/confirm_access

return getUserApprovalPageResponse(model, authorizationRequest, (Authentication) principal);

1.2.3 资源所有者批准,重定向返回授权码

用户批准后,被 AuthorizationEndpoint.approveOrDeny() 方法处理,返回授权码,并重定向用户设置的地址(/login),并带上code和state

return getAuthorizationCodeResponse(authorizationRequest, (Authentication) principal);

1.2.4 客户端获取到授权码,请求Token

在客户端 AbstractAuthenticationProcessingFilter 中处理

authResult = attemptAuthentication(request, response);

由子类 OAuth2ClientAuthenticationProcessingFilter.attemptAuthentication() 处理,判断token是否为空

accessToken = restTemplate.getAccessToken();

如果为空,在 AuthorizationCodeAccessTokenProvider.obtainAccessToken() 方法中,获取返回的授权码,向auth-server请求Token

return retrieveToken(request, resource, getParametersForTokenRequest(resource, request),getHeadersForTokenRequest(request));

在auth-server中 TokenEndpoint.getAccessToken() 方法获取token,进行客户端校验后生成token并返回

OAuth2AccessToken token = getTokenGranter().grant(tokenRequest.getGrantType(), tokenRequest);

1.2.5 获取到Token,重定向 /user

回到在客户端 OAuth2ClientAuthenticationProcessingFilter.attemptAuthentication() 中,获取到token后,带上token,向auth-server请求用户信息。
默认Token是使用uuid,生成用于认证的token和刷新的Token。认证Token默认12小时过期,刷新的Token默认30天过期。

OAuth2Authentication result = tokenServices.loadAuthentication(accessToken.getValue());

在auth-server 被 OAuth2AuthenticationProcessingFilter 处理,从头部获取并验证token后,完成该请求。

客户端获取到用户信息,在客户端重新完成登录的流程,最后在默认的登录成功Handler中获取到重定向地址(即 /user),并重定向。

1.3 JWT Token

1.3.1 资源服务器未添加tokenServices

只需要修改auth-server中授权服务器。

添加依赖

<dependency><groupId>org.springframework.security</groupId><artifactId>spring-security-jwt</artifactId><version>1.0.11.RELEASE</version>
</dependency>

自定义生成token携带的信息

@Component
public class CustomTokenEnhancer implements TokenEnhancer {@Overridepublic OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication) {final Map<String, Object> additionalInfo = new HashMap<>(2);UserDetails user = (UserDetails) authentication.getUserAuthentication().getPrincipal();additionalInfo.put("userName", user.getUsername());additionalInfo.put("authorities", user.getAuthorities());((DefaultOAuth2AccessToken) accessToken).setAdditionalInformation(additionalInfo);return accessToken;}
}

修改 AuthorizationServerConfig

@Autowired
private CustomTokenEnhancer customTokenEnhancer;@Autowired
private AuthenticationManager authenticationManager;@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {// token增强配置TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();tokenEnhancerChain.setTokenEnhancers(Arrays.asList(customTokenEnhancer, jwtAccessTokenConverter()));endpoints// 令牌存在redis.tokenStore(tokenStore()).tokenEnhancer(tokenEnhancerChain)// 密码授权方式时需要.authenticationManager(authenticationManager)// /oauth/token 运行get和post.allowedTokenEndpointRequestMethods(HttpMethod.GET, HttpMethod.POST);
}/*** 用来生成token的转换器* @return*/
@Bean
public JwtAccessTokenConverter jwtAccessTokenConverter() {JwtAccessTokenConverter jwtAccessTokenConverter = new JwtAccessTokenConverter();// 对称加密,设置签名,使用下面这个值作为密钥jwtAccessTokenConverter.setSigningKey("oauth");return jwtAccessTokenConverter;
}

添加客户端2,支持密码授权方式

INSERT INTO `oauth_client_details`(`client_id`, `resource_ids`, `client_secret`, `scope`, `authorized_grant_types`, `web_server_redirect_uri`, `authorities`, `access_token_validity`, `refresh_token_validity`, `additional_information`, `autoapprove`) VALUES ('client_2', 'resource-1', '$2a$10$TfM5Bisse4ewbmIDfqZcxuYl5dI39/lEzzvkzlxFELKglHQM78FIu', 'read', 'password,refresh_token', NULL, NULL, NULL, NULL, NULL, NULL);

测试
使用密码模式获取token
获取token
使用token获请求资源服务器保护的接口测试token
流程
在auth-server的 TokenEndpoint 中验证信息并获取token。然后带着token请求,在service-1中被 OAuth2AuthenticationProcessingFilter 处理,doFilter() 方法会提取并验证token。

按上面的配置,并没有在资源服务器中配置tokenServices

Authentication authResult = authenticationManager.authenticate(authentication);

所以在加载 Authentication 的时候,tokenServices 为 UserInfoTokenServices,就会调用配置的 userInfoUri 去auth-server获取用户信息

OAuth2Authentication auth = tokenServices.loadAuthentication(token);

1.3.2 资源服务器添加tokenServices

auth-server
修改ResourceServerConfig

@Autowired
private TokenStore tokenStore;@Override
public void configure(ResourceServerSecurityConfigurer resources) throws Exception {DefaultTokenServices defaultTokenServices = new DefaultTokenServices();defaultTokenServices.setTokenStore(tokenStore);resources.resourceId(RESOURCE_ID).tokenServices(defaultTokenServices);
}

service-1
添加依赖

<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId>
</dependency><!--spring2.0集成redis所需common-pool2-->
<dependency><groupId>org.apache.commons</groupId><artifactId>commons-pool2</artifactId><version>2.4.2</version>
</dependency>

修改application.yml

spring:redis:# Redis默认情况下有16个分片,这里配置具体使用的分片,默认是0database: 0host: localhostport: 6379# 连接密码(默认为空)password:# 连接超时时间(毫秒)timeout: 10000mslettuce:pool:# 连接池最大连接数(使用负值表示没有限制) 默认 8max-active: 8# 连接池最大阻塞等待时间(使用负值表示没有限制) 默认 -1max-wait: -1# 连接池中的最大空闲连接 默认 8max-idle: 8# 连接池中的最小空闲连接 默认 0min-idle: 0

修改 ResourceServerConfig

@Autowired
private RedisConnectionFactory redisConnectionFactory;@Override
public void configure(ResourceServerSecurityConfigurer resources) throws Exception {DefaultTokenServices defaultTokenServices = new DefaultTokenServices();defaultTokenServices.setTokenStore(tokenStore());resources.resourceId(RESOURCE_ID).tokenServices(defaultTokenServices);
}/*** 配置redis,使用redis存token* @return*/
@Bean
public TokenStore tokenStore(){return new RedisTokenStore(redisConnectionFactory);
}

流程
在auth-server的 TokenEndpoint 中验证信息并获取token。然后带着token请求,在service-1中被 OAuth2AuthenticationProcessingFilter 处理,doFilter() 方法会提取并验证token。

按上面的配置,并没有在资源服务器中配置tokenServices

Authentication authResult = authenticationManager.authenticate(authentication);

所以在加载 Authentication 的时候,tokenServices 为 DefaultTokenServices,再加上有UserDetails的实现类,可以解析,就不用在调用auth-server

OAuth2Authentication auth = tokenServices.loadAuthentication(token);

2. 单点登出

这里除了部分的资源服务器中配置的api需要token验证,其他还是依赖于Spring Security的认证。而Spring Security是使用Cookie和Session的记录用户。所以可以将认证中心和各个子系统的Cookie设置在同一路径下,在认证中心登出时,将Cookie一并删除,实现认证中心和各个子系统的登出。各子系统需要知道认证中心的登出地址。在这里是http://localhost:8000/auth-server/logout。

修改认证中心和各个子系统的Cookie路径,测试发现,放在 / 下才可实现

server:servlet:session:cookie:path: /

在auth-server添加登出成功的Handler

@Component
public class CustomLogoutSuccessHandler extends AbstractAuthenticationTargetUrlRequestHandler implements LogoutSuccessHandler {@Overridepublic void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {// 将子系统的cookie删掉Cookie[] cookies = request.getCookies();if(cookies != null && cookies.length>0){for (Cookie cookie : cookies){cookie.setMaxAge(0);cookie.setPath("/");response.addCookie(cookie);}}super.handle(request, response, authentication);}
}

修改auth-server的ServerWebSecurityConfig,添加logout配置

@Configuration
public class ServerWebSecurityConfig extends WebSecurityConfigurerAdapter {@Autowiredprivate CustomLogoutSuccessHandler customLogoutSuccessHandler;@Overrideprotected void configure(HttpSecurity http) throws Exception {http....and()// 默认为 /logout.logout().logoutSuccessHandler(customLogoutSuccessHandler)// 无效会话.invalidateHttpSession(true)// 清除身份验证.clearAuthentication(true).permitAll()...;}
}

当然,使用了OAuth发放token,应该也需要使token失效。

@Autowired
private TokenStore tokenStore;@GetMapping("/revokeToken")
public void revokeToken(HttpServletRequest request) {String authHeader = request.getHeader("Authorization");if (authHeader != null) {String tokenValue = authHeader.replace("Bearer", "").trim();OAuth2AccessToken accessToken = tokenStore.readAccessToken(tokenValue);tokenStore.removeAccessToken(accessToken);}
}

3. 总结

  1. AuthorizationEndpoint 处理 /oauth/authorize;TokenEndpoint 处理 /oauth/token。

  2. @EnableOAuth2Sso 会将资源服务器标记为OAuth 2.0的客户端, 它将负责将资源所有者(最终用户)重定向到用户必须输入其凭据的授权服务器。完成后,用户将被重定向回具有授权码的客户端。然后客户端通过调用授权服务器获取授权代码并将其交换为访问令牌。只有在此之后,客户端才能使用访问令牌调用资源服务器。

  3. @EnableResourceServer 意味着所属的服务需要访问令牌才能处理请求。在调用资源服务器之前,需要先从授权服务器获取访问令牌。

  4. 在资源服务器中配置的路径,都会被 OAuth2AuthenticationProcessingFilter 处理,获取token。

  5. 之前一直在纠结,客户端获取到了token,为什么在访问 /user 的请求头中并没有Authorization,亦可请求成功。其实都因为Security。没有在资源服务器中配置的路径,登录认证成功后并不需要携带token,而还是使用Security需要的Cookie和Session。

  6. 如果资源服务器没有配置tokenService,就会调用配置的userInfoUri去auth-server获取用户信息;如果资源服务器配置了tokenService,再加上有UserDetails的实现类,可以解析,就不用在调用auth-server的接口。

参考:
Spring Security Oauth2和Spring Boot实现单点登录
Spring Security Oauth2 单点登录案例实现和执行流程剖析
Spring Security OAuth2 入门
Spring security. How to log out user (revoke oauth2 token)
从零开始的Spring Security Oauth2(一)
从零开始的Spring Security Oauth2(二)
从零开始的Spring Security Oauth2(三)
Spring Security OAuth2 入门
Spring Security EnableOAuth2Sso注解实现原理
Spring Security OAuth2 使用Redis存储token键值详解
Spring Security OAuth2实现使用JWT
jwt 官网
jwt 解码器


http://chatgpt.dhexx.cn/article/T2t6mDLi.shtml

相关文章

OneNote的正确登出方式

1. 账号已经是登录状态 点击oneNote右上角账户信息 》 注销按钮 &#xff08;退出登录&#xff0c;注意要确保笔记已经同步到云端&#xff09; 》会弹出以下提示窗口 》点击是 2. 关闭笔记本 点击左上角 》 文件 》信息》选中自己的笔记本 》设置 》关闭&#xff08;可防止…

springboot2.7+springsecurity实现简单的登录登出

由于网上搜索的资料大部分都是使用的springsecurity的低于5.7的版本&#xff0c;springsecurity的配置类是通过继承WebSecurityConfigurerAdapter这个类来重写configure&#xff0c;以此来实现认证和授权。近期学习springsecurity使用的是5.7的版本&#xff0c;所以简单整理一下…

Vue实现登录以及登出

首先先了解一下&#xff0c;我们的效果实现流程 首先登录概述及业务流程和相关技术点 1.登录页面的布局 2.创建两个Vue.js文件 3.一个我们来做登录页和注册页 4.登录页面的布局 5.配置路由 6…

iOS快捷指令:一键登录/登出南京大学校园网

软件版本要求&#xff1a;iOS13及以上 演示机型&#xff1a;iPhone 12 mini 演示系统版本&#xff1a;iOS14.6 文章目录 导言核心步骤&#xff1a;最简单的一键登录指令自动化&#xff1a;连接NJU-WLAN后自动登录修改细节&#xff1a;让指令变得更优雅小练习&#xff1a;创建…

瑞吉外卖 —— 2、后台登录和登出

目录 1、后台登录功能 1.1、接口分析 1.1.2、登录校验逻辑 1.2、代码 1.2.1、统一的返回结果实体类 1.2.2、controller 方法 1.3、测试 2、后台退出功能 2.1、分析 2.2、代码 3、未登录访问首页跳转到登录页面 3.1、分析 3.2、代码 1、后台登录功能 1.1、接口分析…

oc账号无法登出,oc登出后官网还显示登陆状态?

《C4D的十万个为什么》首发于 公众号&#xff1a;苦七君 免费搜索查看更多问题&#xff1a;kuqijun.com 问题&#xff1a; 在本电脑上&#xff0c;在C4D里面登出oc账号后&#xff0c;官网上还是显示登陆的。。导致账号被限制在此电脑上了&#xff0c;无法用其他电脑登陆。 正…

vue-admin-实现登出功能

实现登出功能 目标&#xff1a;实现用户的登出操作 登出仅仅是跳到登录页吗&#xff1f; 不&#xff0c;当然不是&#xff0c;我们要处理如下 同样的&#xff0c;登出功能&#xff0c;我们在vuex中的用户模块中实现对应的action 登出action src/store/modules/user.js // 登出…

Python + Django4 搭建个人博客(十四):实现用户登录和登出功能

本篇开始我们来实现用户管理模块。 首先我们来实现一下用户的登录和登出。 创建APP 用户和文章属于不同的功能模块&#xff0c;为了方便管理&#xff0c;我们新建一个名为userprofile的App 运行startapp指令创建新的app&#xff1a; python manage.py startapp userprofile…

Springboot+JWT+Redis实现登陆登出功能

1&#xff1a;什么是Token&#xff1f;&#xff1a;三部分组成&#xff1a;头有效负载签名 1.1 JWT创建中的一些方法讲解&#xff1a; public static String createTokenWithClaim(User user){//构建头部信息Map<String,Object> map new HashMap<>();map.put(&qu…

单点登录与单点登出

一、标准流程描述 CAS官网的标准流程&#xff1a; SSO标准流程 流程描述&#xff1a; First Access&#xff1a; 第3步数据走向 第4步数据走向 第一次访问app.example.com&#xff08;service地址&#xff09;&#xff0c;请求参数中session为空&#xff0c;app service没做…

[django项目] 实现用户登录登出功能

用户登录登出功能 I. 功能需求分析 1>功能分析 1.1>流程图 1.2>功能接口 登录页面登录功能退出功能 II. 登陆页面 1>接口设计 1.1>接口说明 类目说明请求方法GETurl定义/users/login/参数格式无参数 1.2>返回结果 登陆页面 2.后端代码 user/views…

后台登录登出

后台登录登出 一&#xff0e;Session简介 在WEB开发中&#xff0c;服务器可以为每个用户浏览器创建一个会话对象&#xff08;session对象&#xff09;&#xff0c;注意&#xff1a;一个浏览器独占一个session对象(默认情况下)。因此&#xff0c;在需要保存用户数据时&#xff…

JWT 的登出问题

Jwt 使用起来不难&#xff0c;而且让我们将“无状态”的概念更贴切的展示出来了&#xff0c;但是实践就真的这么完美吗&#xff1f;不是&#xff0c;因为jwt 的登出问题。 何为登出&#xff1a;就是用户自己点击登出后&#xff0c;或用户的角色/权限改变后&#xff0c;该token…

Shiro入门之实现登录登出

概述 这里使用Shiro来实现用户的登录和登出功能。 前提&#xff1a;已经会Spring集成Shiro。即使没有下面也会提供源码&#xff0c;下面只说明Shiro部分的核心代码&#xff0c;如Mapper、Service类中的代码基本上就是从数据库中读取数据&#xff0c;而且源码有提供&#xff0…

cas5.3.2单点登录-单点登出(十一)

原文地址&#xff0c;转载请注明出处&#xff1a; https://blog.csdn.net/qq_34021712/article/details/81515317 ©王赛超 既然有单点登录,肯定就要有登出,之前的整合都是只针对了登录&#xff0c;对登出并没有关注,今天我们就来讲讲登出。 关于单点登出原理&#…

数说故事车企数字化渠道管理创新方法——精准进行消费者洞察

随着疫情带来的变化&#xff0c;原来在一二线城市的购物中心店&#xff0c;受人流量的不确定性冲击越来越大&#xff0c;但成本的支出也越来越高。因此购物中心店&#xff0c;将有可能从原来的重“集客”功能&#xff0c;变成更多的从品牌、体验出发的形象中心店&#xff0c;“…

权威报告!这五个消费趋势,告诉你如何抓住中国消费者的心和钱包

有人说2023年是消费复苏的一年&#xff0c;市场回暖趋势明显&#xff1b;也有人说之前的亏空太大&#xff0c;想要短时间追上来不太可能&#xff0c;因此2023的消费市场最多是不低迷&#xff0c;达不到火热。 这可把做生意的各位老板整纠结了&#xff0c;究竟今年要不要投个大手…

ChatGPT与数据挖掘:洞察消费者行为,优化营销策略

随着科技的不断进步和数字化时代的到来&#xff0c;企业们越来越意识到数据的重要性。在零售和电子商务行业&#xff0c;了解消费者行为并准确洞察其需求&#xff0c;是成功营销和提升业绩的关键。而现在&#xff0c;借助人工智能技术中的ChatGPT以及数据挖掘技术&#xff0c;企…

市场营销学5——消费者购买行为分析

什么是消费者购买行为 消费者购买行为是指人们为满足需要和欲望而寻找、选择、购买、使用、评价及处置产品、服务时介入的过程活动&#xff0c;包括消费者的主观心理活动和客观物质活动两个方面。 消费者购买行为分析的环节 消费者购买行为研究包括以下几个环节&#xff1a; 购…

【消费战略方法论】认识消费者的恒常原理(三):消费者刺激反馈原理

人类是一种高度智能的生物&#xff0c;而所谓智能的核心在于其理解世界的能力&#xff0c;而理解世界的过程中必然伴随着感知和反应。人的刺激反馈机制就是在这个过程中发挥着重要的作用。 刺激反馈机制是一种生物学的反应现象&#xff0c;它指的是人体对外界刺激的感知与反应…