# SSO
**Repository Path**: islands_yang/sso
## Basic Information
- **Project Name**: SSO
- **Description**: Springboot3.0+Security+Oauth2.1
- **Primary Language**: Java
- **License**: Apache-2.0
- **Default Branch**: master
- **Homepage**: None
- **GVP Project**: No
## Statistics
- **Stars**: 0
- **Forks**: 3
- **Created**: 2024-09-19
- **Last Updated**: 2025-02-24
## Categories & Tags
**Categories**: Uncategorized
**Tags**: None
## README
SSO(Springboot3.0+Security+Oauth2.1)
#### 介绍
### 一、认证/授权服务器搭建
**Spring Authorization Server重要组件:**
- SecurityFilterChain -> authorizationServerSecurityFilterChain Spring Security的过滤器链,用于协议端点的。
- SecurityFilterChain -> defaultSecurityFilterChain Spring Security的过滤器链,用于Spring Security的身份认证
- UserDetailsService 主要进行用户身份验证
- RegisteredClientRepository 主要用于管理客户端
- JWKSource 用于签名访问令牌
- KeyPair 启动时生成的带有密钥的KeyPair实例,用于创建上面的JWKSource
- JwtDecoder JwtDecoder的一个实例,用于解码已签名的访问令牌
- AuthorizationServerSettings 用于配置Spring Authorization Server的AuthorizationServerSettings实例。
**搭建**
```pom
#关键依赖
org.springframework.boot
spring-boot-starter-oauth2-authorization-server
```
**ymal配置**
```yaml
server:
port: 9000
```
**授权服务器配置**
```java
package com.tuling.authserver.config;
import com.nimbusds.jose.jwk.JWKSet;
import com.nimbusds.jose.jwk.RSAKey;
import com.nimbusds.jose.jwk.source.ImmutableJWKSet;
import com.nimbusds.jose.jwk.source.JWKSource;
import com.nimbusds.jose.proc.SecurityContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.http.MediaType;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
import org.springframework.security.oauth2.core.oidc.OidcScopes;
import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.security.oauth2.server.authorization.client.InMemoryRegisteredClientRepository;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
import org.springframework.security.oauth2.server.authorization.config.annotation.web.configuration.OAuth2AuthorizationServerConfiguration;
import org.springframework.security.oauth2.server.authorization.config.annotation.web.configurers.OAuth2AuthorizationServerConfigurer;
import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings;
import org.springframework.security.oauth2.server.authorization.settings.ClientSettings;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint;
import org.springframework.security.web.util.matcher.MediaTypeRequestMatcher;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import java.util.UUID;
/**
* https://docs.spring.io/spring-authorization-server/docs/1.1.2-SNAPSHOT/reference/html/getting-started.html
*/
@Configuration
@EnableWebSecurity
public class SecurityConfig {
/**
* Spring Authorization Server 相关配置
* 主要配置OAuth 2.1和OpenID Connect 1.0
* @param http
* @return
* @throws Exception
*/
@Bean
@Order(1)
public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http)
throws Exception {
OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);
http.getConfigurer(OAuth2AuthorizationServerConfigurer.class)
//开启OpenID Connect 1.0(其中oidc为OpenID Connect的缩写)
.oidc(Customizer.withDefaults()); // Enable OpenID Connect 1.0
http
// Redirect to the login page when not authenticated from the
// authorization endpoint
//将需要认证的请求,重定向到login进行登录认证。
.exceptionHandling((exceptions) -> exceptions
.defaultAuthenticationEntryPointFor(
new LoginUrlAuthenticationEntryPoint("/login"),
new MediaTypeRequestMatcher(MediaType.TEXT_HTML)
)
)
// Accept access tokens for User Info and/or Client Registration
// 使用jwt处理接收到的access token
.oauth2ResourceServer((resourceServer) -> resourceServer
.jwt(Customizer.withDefaults()));
return http.build();
}
/**
* Spring Security 过滤链配置(此处是纯Spring Security相关配置)
*/
@Bean
@Order(2)
public SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http)
throws Exception {
http
.authorizeHttpRequests((authorize) -> authorize
//设置所有请求都需要认证,未认证的请求都被重定向到login页面进行登录
.anyRequest().authenticated()
)
// Form login handles the redirect to the login page from the
// authorization server filter chain
// 由Spring Security过滤链中UsernamePasswordAuthenticationFilter过滤器拦截处理“login”页面提交的登录信息。
.formLogin(Customizer.withDefaults());
return http.build();
}
/**
* Spring Security的配置
* 设置用户信息,校验用户名、密码
* 正常的流程是自定义一个service类,实现UserDetailsService接口,去查询DB 查询用户信息,封装为一个UserDetails对象返回
* 这里就直接写一个user存入内存中进行测试
* @return
*/
@Bean
public UserDetailsService userDetailsService() {
UserDetails userDetails = User.withDefaultPasswordEncoder()
.username("admin")
.password("admin123")
.roles("USER")
.build();
//基于内存的用户数据校验
return new InMemoryUserDetailsManager(userDetails);
}
/**
* 注册客户端信息
*
* 查询认证服务器信息
* http://127.0.0.1:9000/.well-known/openid-configuration
*
* 获取授权码
* http://localhost:9000/oauth2/authorize?response_type=code&client_id=oidc-client&scope=profile openid&redirect_uri=http://www.baidu.com
*
* 正常的流程是存在一个web前端页面提供给客户端进行注册,然后后端接口会将客户端注册信息保存在DB中,然后去查询DB,最后封装为一个RegisteredClient
* 我这里就直接写一个客户端,存放在内存中进行测试
*/
@Bean
public RegisteredClientRepository registeredClientRepository() {
RegisteredClient oidcClient = RegisteredClient.withId(UUID.randomUUID().toString())
.clientId("oidc-client")
//{noop}开头,表示“secret”以明文存储
.clientSecret("{noop}Islands77")
// 就使用默认的认证方式
.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
// 配置授权码模式,刷新令牌,客户端模式
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
.authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
.authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
.redirectUri("http://spring-oauth-client:9001/login/oauth2/code/messaging-client-oidc")
//我们暂时还没有客户端服务,以免重定向跳转错误导致接收不到授权码
.redirectUri("http://www.baidu.com")
.postLogoutRedirectUri("http://127.0.0.1:8080/")
//设置客户端权限范围
.scope(OidcScopes.OPENID)
.scope(OidcScopes.PROFILE)
//客户端设置用户需要确认授权
.clientSettings(ClientSettings.builder().requireAuthorizationConsent(true).build())
.build();
//配置基于内存的客户端信息
return new InMemoryRegisteredClientRepository(oidcClient);
}
/**
* 配置 JWK,为JWT(id_token)提供加密密钥,用于加密/解密或签名/验签
* JWK详细见:https://datatracker.ietf.org/doc/html/draft-ietf-jose-json-web-key-41
*/
@Bean
public JWKSource jwkSource() {
KeyPair keyPair = generateRsaKey();
RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();
RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate();
RSAKey rsaKey = new RSAKey.Builder(publicKey)
.privateKey(privateKey)
.keyID(UUID.randomUUID().toString())
.build();
JWKSet jwkSet = new JWKSet(rsaKey);
return new ImmutableJWKSet<>(jwkSet);
}
/**
* 生成RSA密钥对,给上面jwkSource() 方法的提供密钥对
*/
private static KeyPair generateRsaKey() {
KeyPair keyPair;
try {
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
keyPairGenerator.initialize(2048);
keyPair = keyPairGenerator.generateKeyPair();
}
catch (Exception ex) {
throw new IllegalStateException(ex);
}
return keyPair;
}
/**
* 配置jwt解析器
*/
@Bean
public JwtDecoder jwtDecoder(JWKSource jwkSource) {
return OAuth2AuthorizationServerConfiguration.jwtDecoder(jwkSource);
}
/**
* 配置授权服务器请求地址
*/
@Bean
public AuthorizationServerSettings authorizationServerSettings() {
//什么都不配置,则使用默认地址
return AuthorizationServerSettings.builder().build();
}
}
```
**测试**
```java
//查询认证服务器信息
http://127.0.0.1:9000/.well-known/openid-configuration
//向授权服务器获取授权码,其中/oauth2/authorize也是内部提供的接口
# response_type=code 表示当前是授权码模式
# client_id、scope这两个请求参数要和客户端往授权服务器注册信息对应上,如果客户端注册时没有profile openid中的某一个,那么授权服务器就不会返回code
# redirect_uri 重定向地址,也就是用户授权之后 code应该发给谁,目前我们还没有编写客户端的接口,所以这里就直接写百度,然后从浏览器拿code进行测试
http://localhost:9000/oauth2/authorize?response_type=code&client_id=oidc-client&scope=profile openid&redirect_uri=http://www.baidu.com
//授权码模式获取令牌
POST http://localhost:9000/oauth2/token
body:{
grant_type:authorization_code
code:授权吗
redirect_uri:http://www.baidu.com
}
Auth:{
type:Basic Auth
username: oidc-client
password:Islands77
}
```
### 二、oauth2客户端搭建
**搭建客户端**
因为我是在同一台机器上进行启动客户端和认证授权服务器的,ip都是127.0.0.1,在ip相同的情况下,会出现cookie覆盖的情形,这会导致认证服务器重定向到客户端地址时会出现[authorization_request_not_found]异常,为解决这个问题,可以在hosts文件添加了一行IP域名映射
```shell
127.0.0.1 sso-server sso-client
```
**Spring Security OAuth2 Client组件介绍:**
- ClientRegistration 注册的客户端
- ClientRegistrationRepository ClientRegistration的存储仓库
- OAuth2AuthorizedClient 已授权过的客户端
- OAuth2AuthorizedClientRepository 已授权过的客户端存储库持久化
- OAuth2AuthorizationRequestRedirectFilter 该过滤器处理 /oauth2/authorization 路径,转发给 认证中心 对应的路径 /oauth2/authorize
- OAuth2AuthorizationCodeGrantFilter 负责处理 认证中心 的授权码回调请求,如地址重定向
- OAuth2LoginAuthenticationFilter 处理第三方认证的回调(该回调有授权码),拿着授权码到第三方认证服务器获取access_token和refresh_token
**搭建**
```pom
#关键依赖
org.springframework.boot
spring-boot-starter-oauth2-client
org.springframework.boot
spring-boot-starter-web
```
**ymal配置**
```yaml
server:
port: 9001
spring:
application:
name: sso-client
security:
oauth2:
client:
provider:
#认证服务器信息,下面这个string可以自定义,但是要和下方messaging-client-oidc.provider的对应上
oauth-server:
#授权地址
issuer-uri: http://sso-server:9000
authorizationUri: ${spring.security.oauth2.client.provider.oauth-server.issuer-uri}/oauth2/authorize
#令牌获取地址
tokenUri: ${spring.security.oauth2.client.provider.oauth-server.issuer-uri}/oauth2/token
registration:
# 下面这个string可以自定义,这里自定义的要和最下面redirect-uri的最后一个请求地址对应上
messaging-client-oidc:
#认证提供者,标识由哪个认证服务器进行认证,和上面的oauth-server进行关联
provider: oauth-server
#客户端名称
client-name: web平台
#客户端id,从认证平台申请的客户端id
client-id: oidc-client
#客户端秘钥
client-secret: Islands77
#客户端认证方式
client-authentication-method: client_secret_basic
#使用授权码模式获取令牌(token)
authorization-grant-type: authorization_code
#回调地址,接收认证服务器回传code的接口地址,之前我们是使用http://www.baidu.com代替
# /login/oauth2/code/messaging-client-oidc 这个接口是使用的oauth2-client依赖默认提供的接口
# 这里最后的messaging-client-oidc 需要和上方我们自定义是string对应上
# 该接口会收到code授权码之后,会去调用授权服务器获取token,
# 也就是我们上方配置的${spring.security.oauth2.client.provider.oauth-server.tokenUri}
# 也可以使用自定义的接口,只不过需要我们自己拿到code之后再去调用资源服务器获取token
redirect-uri: http://sso-client:9001/login/oauth2/code/messaging-client-oidc
scope:
- profile
- openid
```
**授权服务器配置**
```java
@RestController
public class AuthenticationController {
@GetMapping("/token")
@ResponseBody
public OAuth2AuthorizedClient token(@RegisteredOAuth2AuthorizedClient OAuth2AuthorizedClient oAuth2AuthorizedClient) {
//通过OAuth2AuthorizedClient对象获取到客户端和token令牌相关的信息,然后直接返回给前端页面
return oAuth2AuthorizedClient;
}
}
```
**测试**
```java
启动认证服务器和客户端服务,浏览器输入http://sso-client:9001/token地址发起接口访问。此时我们会看到,浏览器地址被重定向到认证服务器http://sso-server:9000/login中被要求进行登录,登陆后,勾选授权信息profile,点击提交按钮,则返回如下结果
此时我们看到了浏览器中的地址为http://sso-client:9001/token?continue,且返回了客户端及token信息。也看到了认证服务器将授权码拼接到了回调地址http://sso-client:9001/login/oauth2/code/messaging-client-oidc中传给客户端。
```
### 三、资源服务器
**说明**
资源服务器在分布式服务中就是指用户服务、商品服务、订单服务那些了,访问资源服务器中受保护的资源,都需要带上令牌(token)进行访问。
资源服务器往往和客户端一起配合使用,客户端侧重于身份认证,资源服务器侧重于权限校验,如果在Spring Cloud(Alibaba)微服务架构中,可以将客户端框架spring-boot-starter-oauth2-client集成到网关服务上,将资源服务器框架spring-boot-starter-oauth2-resource-server集成到用户服务、商品服务、订单服务等微服务上。
**搭建**
```pom
#关键依赖
org.springframework.boot
spring-boot-starter-oauth2-resource-server
```
**ymal配置**
```yaml
server:
port: 9002
logging:
level:
org.springframework.security: trace
spring:
application:
name: resource-server
security:
oauth2:
resource-server:
jwt:
# 资源服务器获取到token之后要去授权服务器进行解析token
issuer-uri: http://sso-server:9000
```
**授权服务器配置**
```java
// 配置所有的请求都需要认证,权限方法就没有在配置类中进行配置了,直接使用注解的方式配置在接口上,所以下面就加上了@EnableMethodSecurity注解
// 其实这里就是SpringSecurity的配置
@Configuration
@EnableWebSecurity
@EnableMethodSecurity(jsr250Enabled = true, securedEnabled = true)
public class ResourceServerConfig {
@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http.authorizeHttpRequests((authorizeHttpRequests) -> authorizeHttpRequests
//所有的访问都需要通过身份认证
.anyRequest().authenticated()
)
// 使用jwt处理接收到的access token
.oauth2ResourceServer((oauth2ResourceServer) -> oauth2ResourceServer
.jwt(Customizer.withDefaults())
);
return http.build();
}
}
```
**测试**
```java
携带令牌访问资源服务器接口,如果没有令牌 则报错401
```
###