Spring Security OAuth2 Provider 之 最小实现

本文介绍使用 SpringBoot 和 SpringSecurityOAuth2 实现 OAuth2.0 授权码模式的流程,包括配置、授权请求、获取令牌及刷新令牌等关键步骤。
不是客户端,而是服务端。

版本:
Spring Boot 1.4.3、Spring Security OAuth2 2.0.12

OAuth2.0的开源 Server / Client 实现可以参考这里:[url=https://oauth.net/code/]https://oauth.net/code/[/url],这里采用Spring Security OAuth2实现四种授权模式中最常用的:Authorization Code Grant。
[img]http://dl2.iteye.com/upload/attachment/0126/1505/7e85834d-81ad-3c25-bbdf-50ff366875b5.png[/img]

具体可以看OAuth2.0标准的定义:[url=https://tools.ietf.org/html/rfc6749#section-4.1]https://tools.ietf.org/html/rfc6749#section-4.1[/url]。

这里首先只为演示 OAuth2.0 的整个过程,做最小实现!

Spring Security OAuth2默认提供的四个URL:
[list][*]/oauth/authorize : 授权AuthorizationEndpoint
[*]/oauth/token : 令牌TokenEndpoint
[*]/oauth/check_token : 令牌校验CheckTokenEndpoint
[*]/oauth/confirm_access : 授权页面WhitelabelApprovalEndpoint
[*]/oauth/error : 错误页面WhitelabelErrorEndpoint[/list]

相关文章:
[url=http://rensanning.iteye.com/blog/2384996]Spring Security OAuth2 Provider 之 最小实现[/url]
[url=http://rensanning.iteye.com/blog/2385162]Spring Security OAuth2 Provider 之 数据库存储[/url]
[url=http://rensanning.iteye.com/blog/2386309]Spring Security OAuth2 Provider 之 第三方登录简单演示[/url]
[url=http://rensanning.iteye.com/blog/2386553]Spring Security OAuth2 Provider 之 自定义开发[/url]
[url=http://rensanning.iteye.com/blog/2386766]Spring Security OAuth2 Provider 之 整合JWT[/url]

[b]代码如下:[/b]

pom.xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</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>
</dependency>


Application.java
@SpringBootApplication
public class Application {

public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}

}


Config.java
public class Config {

public static final String OAUTH_CLIENT_ID = "oauth_client";
public static final String OAUTH_CLIENT_SECRET = "oauth_client_secret";
public static final String RESOURCE_ID = "my_resource_id";
public static final String[] SCOPES = { "read", "write" };

@Configuration
@EnableAuthorizationServer
static class OAuthAuthorizationConfig extends AuthorizationServerConfigurerAdapter {
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory()
.withClient(OAUTH_CLIENT_ID)
.secret(OAUTH_CLIENT_SECRET)
.resourceIds(RESOURCE_ID)
.scopes(SCOPES)
.authorities("ROLE_USER")
.authorizedGrantTypes("authorization_code", "refresh_token")
.redirectUris("http://default-oauth-callback.com")
.accessTokenValiditySeconds(60*30) // 30min
.refreshTokenValiditySeconds(60*60*24); // 24h
}
}

@Configuration
@EnableResourceServer
static class OAuthResourceConfig extends ResourceServerConfigurerAdapter {
@Override
public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
resources.resourceId(RESOURCE_ID);
}
@Override
public void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers(HttpMethod.GET, "/api/**").access("#oauth2.hasScope('read')")
.antMatchers(HttpMethod.POST, "/api/**").access("#oauth2.hasScope('write')");
}
}


@Configuration
@EnableWebSecurity
static class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication()
.withUser("user").password("123").roles("USER")
.and()
.withUser("admin").password("123").roles("ADMIN");
}

@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable();
http.authorizeRequests()
.antMatchers("/oauth/authorize").authenticated()
.and()
.httpBasic().realmName("OAuth Server");
}
}

}


Controller.java
@RestController
public class Controller {

@GetMapping("/api/get")
public String get() {
return "Hello World!";
}

@PostMapping("/api/post")
public String post() {
return "POST process has finished.";
}

@GetMapping("/api/user")
public Object get(HttpServletRequest req) {
SecurityContextImpl sci = (SecurityContextImpl) req.getSession().getAttribute("SPRING_SECURITY_CONTEXT");
if (sci != null) {
Authentication authentication = sci.getAuthentication();
if (authentication != null) {
return authentication.getPrincipal();
}
}
return "none";
}

}


Test.java
public class Test {

public static void main(String[] args) {
System.out.println(generate("oauth_client", "oauth_client_secret"));
}

private static String generate(String clientId, String clientSecret) {
String creds = String.format("%s:%s", new Object[] { clientId, clientSecret });
try {
return "Basic " + new String(Base64.encode(creds.getBytes("UTF-8")));
} catch (UnsupportedEncodingException e) {
throw new IllegalStateException("Could not convert String");
}
}

}


【Run As】【Spring Boot App】启动服务器后,看到以下Log:
[quote]Mapped "{[/oauth/authorize]}" onto public org.springframework.web.servlet.ModelAndView org.springframework.security.oauth2.provider.endpoint.AuthorizationEndpoint.authorize(java.util.Map<java.lang.String, java.lang.Object>,java.util.Map<java.lang.String, java.lang.String>,org.springframework.web.bind.support.SessionStatus,java.security.Principal)
Mapped "{[/oauth/authorize],methods=[POST],params=[user_oauth_approval]}" onto public org.springframework.web.servlet.View org.springframework.security.oauth2.provider.endpoint.AuthorizationEndpoint.approveOrDeny(java.util.Map<java.lang.String, java.lang.String>,java.util.Map<java.lang.String, ?>,org.springframework.web.bind.support.SessionStatus,java.security.Principal)
Mapped "{[/oauth/token],methods=[POST]}" onto public org.springframework.http.ResponseEntity<org.springframework.security.oauth2.common.OAuth2AccessToken> org.springframework.security.oauth2.provider.endpoint.TokenEndpoint.postAccessToken(java.security.Principal,java.util.Map<java.lang.String, java.lang.String>) throws org.springframework.web.HttpRequestMethodNotSupportedException
Mapped "{[/oauth/token],methods=[GET]}" onto public org.springframework.http.ResponseEntity<org.springframework.security.oauth2.common.OAuth2AccessToken> org.springframework.security.oauth2.provider.endpoint.TokenEndpoint.getAccessToken(java.security.Principal,java.util.Map<java.lang.String, java.lang.String>) throws org.springframework.web.HttpRequestMethodNotSupportedException
Mapped "{[/oauth/check_token]}" onto public java.util.Map<java.lang.String, ?> org.springframework.security.oauth2.provider.endpoint.CheckTokenEndpoint.checkToken(java.lang.String)
Mapped "{[/oauth/confirm_access]}" onto public org.springframework.web.servlet.ModelAndView org.springframework.security.oauth2.provider.endpoint.WhitelabelApprovalEndpoint.getAccessConfirmation(java.util.Map<java.lang.String, java.lang.Object>,javax.servlet.http.HttpServletRequest) throws java.lang.Exception
Mapped "{[/oauth/error]}" onto public org.springframework.web.servlet.ModelAndView org.springframework.security.oauth2.provider.endpoint.WhitelabelErrorEndpoint.handleError(javax.servlet.http.HttpServletRequest)[/quote]

[b](1)授权请求(Get)[/b]

URL:[color=blue]http://localhost:8080/oauth/authorize?client_id=oauth_client&scope=read&response_type=code&state=rensanning&redirect_uri=http://default-oauth-callback.com[/color]

由于对/oauth/authorize开启了HTTP Basic认证,所以需要输入密码:
[img]http://dl2.iteye.com/upload/attachment/0126/1306/4e4c033e-821f-337e-93b2-68f4abb46188.png[/img]
输入正确用户名密码(user/123)后显示授权页:
[img]http://dl2.iteye.com/upload/attachment/0126/1308/a8d6a609-ff49-3946-aebc-8acd43253485.png[/img]

选择Approve点击Authorize按钮后,自动跳转到 [color=blue]http://default-oauth-callback.com?code=sdb6vF&state=rensanning[/color]
[img]http://dl2.iteye.com/upload/attachment/0126/1310/cbbe5c3c-e218-3bf4-bb6c-99c7fbcc2325.png[/img]

URL中的code参数值即为授权码,该值是一个6位英数字的随机数,具体可以看源码:org.springframework.security.oauth2.common.util.RandomValueStringGenerator.generate()

!!授权码10分钟过期目前还没有实现!! https://github.com/spring-projects/spring-security-oauth/issues/725

[b](2)获得令牌(Post)[/b]

URL:[color=blue]http://localhost:8080/oauth/token?grant_type=authorization_code&redirect_uri=http://default-oauth-callback.com&code=sdb6vF[/color]

【/oauth/token】默认采用的是HTTP Basic Auth(org.springframework.security.web.authentication.www.BasicAuthenticationFilter),所以需要在HTTP的header里提供clientId和clientSecret的Base64值。具体可以执行Test.java获取。
[b]Authorization: Basic b2F1dGhfY2xpZW50Om9hdXRoX2NsaWVudF9zZWNyZXQ=[/b]
[img]http://dl2.iteye.com/upload/attachment/0126/1312/62845b31-a4c2-37ff-aca2-9685f56e206f.png[/img]

通过以下设置,可以通过参数的形式传递clientId和clientSecret的值。比如:http://localhost:8080/oauth/token?grant_type=authorization_code&redirect_uri=http://default-oauth-callback.com&code=sdb6vF&client_id=oauth_client&client_secret=oauth_client_secret
@Configuration
@EnableAuthorizationServer
protected static class OAuth2Config extends AuthorizationServerConfigurerAdapter {
@Override
public void configure(AuthorizationServerSecurityConfigurer oauthServer) throws Exception {
oauthServer.allowFormAuthenticationForClients();
}
}


返回令牌信息
[quote] {
"access_token": "1cc5ffbd-faac-4d20-afd9-b8531acd248e",
"token_type": "bearer",
"refresh_token": "5b319fed-5600-4ea2-8c4f-61f6e3ea6e41",
"expires_in": 1631,
"scope": "read"
}[/quote]
生成的令牌是一个UUID,具体可以看源码:org.springframework.security.oauth2.provider.token.DefaultTokenServices.createAccessToken()

[b](4)访问API(Get)[/b]

直接访问API返回401。URL:[color=blue]http://localhost:8080/api/get[/color]
[img]http://dl2.iteye.com/upload/attachment/0126/1314/4f22d441-01a0-35df-adbb-313b6246020d.png[/img]

通过access_token参数访问。URL:[color=blue]http://localhost:8080/api/get?access_token=1cc5ffbd-faac-4d20-afd9-b8531acd248e[/color]
[img]http://dl2.iteye.com/upload/attachment/0126/1316/583e7667-f5c0-3b73-ae33-250be9dda897.png[/img]

通过http header参数访问。URL:[color=blue]http://localhost:8080/api/get[/color]
[b]Authorization: Bearer 1cc5ffbd-faac-4d20-afd9-b8531acd248e[/b]
[img]http://dl2.iteye.com/upload/attachment/0126/1318/ce47a3d7-e76e-38a2-83ae-20891b6e8f79.png[/img]

*** @EnableResourceServer 自动增加OAuth2AuthenticationProcessingFilter过滤器
*** !!SpringBoot1.5 @EnableResourceServer和@EnableWebSecurity配置的HttpSecurity有先后顺序的问题,需要特殊设置!!参考:
https://github.com/spring-projects/spring-security-oauth/issues/993#issuecomment-284430752
https://stackoverflow.com/questions/29893602/spring-security-form-logging-and-outh2-in-same-app

[b](5)刷新令牌(Post)[/b]

URL:[color=blue]http://localhost:8080/oauth/token?grant_type=refresh_token&refresh_token=5b319fed-5600-4ea2-8c4f-61f6e3ea6e41[/color]
[b]Authorization: Basic b2F1dGhfY2xpZW50Om9hdXRoX2NsaWVudF9zZWNyZXQ=[/b]

返回新的access_token:
[quote] {
"access_token": "3cbe70fc-753f-44ff-9bb4-0ba6bc3c9aab",
"token_type": "bearer",
"refresh_token": "5b319fed-5600-4ea2-8c4f-61f6e3ea6e41",
"expires_in": 1800,
"scope": "read"
}[/quote]
[img]http://dl2.iteye.com/upload/attachment/0126/1320/336eca97-f62a-3ccd-af9b-b8136c6d40f4.png[/img]

旧的Token就不能再用了:
[img]http://dl2.iteye.com/upload/attachment/0126/1322/319097e5-2863-3adc-ad76-f0cdeaba1acc.png[/img]

通过新的Token访问API:
[img]http://dl2.iteye.com/upload/attachment/0126/1324/6928357b-2e2c-3fa0-afb2-8f760b15bd8f.png[/img]

同理,可以测试scope为write的权限!

参考:
http://projects.spring.io/spring-security-oauth/docs/oauth2.html
https://segmentfault.com/a/1190000010540911
http://ifeve.com/oauth2-tutorial-all/
http://websystique.com/spring-security/secure-spring-rest-api-using-oauth2/
http://qiita.com/TakahikoKawasaki/items/200951e5b5929f840a1f
https://murashun.jp/blog/20150920-01.html
<think>好的,我现在需要帮助用户了解如何使用Spring SecurityOAuth2实现单点登录。首先,我得回顾一下用户提供的引用内容,确保回答准确。引用[1]提到需要创建客户端工程并引入相关依赖,引用[2]涉及JWT自定义内容,引用[3]提到了过滤器,引用[4]则提到了认证服务器和客户端的配置。 首先,我应该分步骤说明实现过程。单点登录的核心是认证服务器和多个客户端。需要先配置认证服务器,处理用户登录并颁发令牌。然后配置各个客户端,使其能够通过认证服务器进行验证。 接下来,要确保依赖正确。根据引用[1],客户端需要引入spring-boot-starter-oauth2-clientsecurityoauth2-autoconfigure。同时,认证服务器可能需要不同的依赖,比如授权服务器的支持。 然后,认证服务器的配置是关键。需要设置用户存储、客户端信息和授权类型。这里可能需要使用@EnableAuthorizationServer注解,并配置ClientDetailsService和AuthorizationServerEndpoints。 客户端方面,每个应用需要配置OAuth2客户端,指定认证服务器的地址、客户端ID和密钥,以及重定向URL。同时,Spring Security的配置类需要启用OAuth2登录,并设置登录页面和权限。 关于JWT令牌的自定义,引用[2]提到继承TokenEnhancer,这可以增强令牌的内容,比如添加自定义声明。这部分可能需要创建自定义的TokenEnhancer并在认证服务器配置中启用。 另外,用户可能关心单点登录的实际流程,比如用户访问客户端A,被重定向到认证服务器登录,之后访问客户端B时自动认证。这里需要确保会话和令牌的正确传递,可能涉及Cookie和Session管理。 可能还需要考虑安全性,比如HTTPS的使用,令牌的存储和刷新机制。引用[3]提到的过滤器可能需要配置,以处理OAuth2的认证流程,比如OAuth2ClientAuthenticationProcessingFilter。 最后,引用[4]中的源码示例可能对用户有帮助,可以建议用户参考具体项目结构。同时,生成相关问题,如性能优化、自定义登录页、令牌刷新等,帮助用户深入理解。 需要检查是否有遗漏的步骤,比如是否需要配置资源服务器,或者如何处理令牌的验签。确保所有配置步骤符合OAuth2的标准流程,并正确使用Spring Security的组件。 总结下来,回答的结构应该是:基础概念、实现步骤(认证服务器配置、客户端配置、JWT自定义)、示例代码、页面展示和相关问题。使用代码块展示关键配置,并正确引用提到的来源。</think>### Spring Security OAuth2单点登录实现教程 #### 一、基础概念 单点登录(SSO)通过统一认证中心管理用户身份,用户只需登录一次即可访问多个关联系统[^4]。Spring Security OAuth2通过授权服务器和客户端的配合实现这一机制。 #### 二、实现步骤 1. **认证服务器配置** - 添加依赖: ```xml <dependency> <groupId>org.springframework.security.oauth</groupId> <artifactId>spring-security-oauth2</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> ``` - 配置类示例: ```java @Configuration @EnableAuthorizationServer public class AuthServerConfig extends AuthorizationServerConfigurerAdapter { @Override public void configure(ClientDetailsServiceConfigurer clients) throws Exception { clients.inMemory() .withClient("client1") .secret("{noop}secret1") .redirectUris("http://localhost:8081/login/oauth2/code/sso") .authorizedGrantTypes("authorization_code", "refresh_token") .scopes("read"); } } ``` 2. **客户端配置** - 添加依赖(引用[1]): ```xml <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-oauth2-client</artifactId> </dependency> ``` - 配置YAML: ```yaml spring: security: oauth2: client: registration: sso: client-id: client1 client-secret: secret1 authorization-grant-type: authorization_code redirect-uri: http://localhost:8081/login/oauth2/code/sso scope: read provider: sso: authorization-uri: http://auth-server/oauth/authorize token-uri: http://auth-server/oauth/token ``` 3. **自定义JWT令牌**(引用[2]) ```java public class CustomTokenEnhancer implements TokenEnhancer { @Override public OAuth2AccessToken enhance(OAuth2AccessToken token, OAuth2Authentication authentication) { DefaultOAuth2AccessToken newToken = new DefaultOAuth2AccessToken(token); Map<String, Object> info = new HashMap<>(); info.put("custom_field", "value"); newToken.setAdditionalInformation(info); return newToken; } } ``` #### 三、流程说明 1. 用户访问客户端A时,被重定向到认证服务器登录 2. 认证成功后返回授权码,客户端通过授权码获取访问令牌 3. 访问其他客户端时,通过OAuth2ClientContextFilter自动传递令牌完成认证[^3] #### 四、关键配置项 | 组件 | 核心配置项 | 作用说明 | |----------------|-----------------------------|---------------------------| | 认证服务器 | `@EnableAuthorizationServer` | 开启OAuth2授权服务功能 | | 客户端 | `@EnableOAuth2Client` | 启用OAuth2客户端支持 | | 安全配置 | `http.oauth2Login()` | 配置OAuth2登录认证入口 | #### 五、进阶优化 - 使用JDBC存储客户端信息替代内存存储 - 集成JWT令牌签名验证机制 - 配置`OAuth2ClientAuthenticationProcessingFilter`实现自定义认证逻辑[^3]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值