版本:
Spring Boot 1.4.3、Spring Security OAuth2 2.0.12
OAuth2.0的开源 Server / Client 实现可以参考这里: https://oauth.net/code/,这里采用Spring Security OAuth2实现四种授权模式中最常用的:Authorization Code Grant。

具体可以看OAuth2.0标准的定义: https://tools.ietf.org/html/rfc6749#section-4.1。
这里首先只为演示 OAuth2.0 的整个过程,做最小实现!
Spring Security OAuth2默认提供的四个URL:
- /oauth/authorize : 授权AuthorizationEndpoint
- /oauth/token : 令牌TokenEndpoint
- /oauth/check_token : 令牌校验CheckTokenEndpoint
- /oauth/confirm_access : 授权页面WhitelabelApprovalEndpoint
- /oauth/error : 错误页面WhitelabelErrorEndpoint
相关文章:
Spring Security OAuth2 Provider 之 最小实现
Spring Security OAuth2 Provider 之 数据库存储
Spring Security OAuth2 Provider 之 第三方登录简单演示
Spring Security OAuth2 Provider 之 自定义开发
Spring Security OAuth2 Provider 之 整合JWT
代码如下:
pom.xml
org.springframework.boot spring-boot-starter-web org.springframework.boot spring-boot-starter-security org.springframework.security.oauth spring-security-oauth2
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:
引用
Mapped "{[/oauth/authorize]}" onto public org.springframework.web.servlet.ModelAndView org.springframework.security.oauth2.provider.endpoint.AuthorizationEndpoint.authorize(java.util.Map,java.util.Map,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.util.Map,org.springframework.web.bind.support.SessionStatus,java.security.Principal)
Mapped "{[/oauth/token],methods=[POST]}" onto public org.springframework.http.ResponseEntity org.springframework.security.oauth2.provider.endpoint.TokenEndpoint.postAccessToken(java.security.Principal,java.util.Map) throws org.springframework.web.HttpRequestMethodNotSupportedException
Mapped "{[/oauth/token],methods=[GET]}" onto public org.springframework.http.ResponseEntity org.springframework.security.oauth2.provider.endpoint.TokenEndpoint.getAccessToken(java.security.Principal,java.util.Map) throws org.springframework.web.HttpRequestMethodNotSupportedException
Mapped "{[/oauth/check_token]}" onto public java.util.Map 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,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)
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
Mapped "{[/oauth/token],methods=[POST]}" onto public org.springframework.http.ResponseEntity
Mapped "{[/oauth/token],methods=[GET]}" onto public org.springframework.http.ResponseEntity
Mapped "{[/oauth/check_token]}" onto public java.util.Map
Mapped "{[/oauth/confirm_access]}" onto public org.springframework.web.servlet.ModelAndView org.springframework.security.oauth2.provider.endpoint.WhitelabelApprovalEndpoint.getAccessConfirmation(java.util.Map
Mapped "{[/oauth/error]}" onto public org.springframework.web.servlet.ModelAndView org.springframework.security.oauth2.provider.endpoint.WhitelabelErrorEndpoint.handleError(javax.servlet.http.HttpServletRequest)
(1)授权请求(Get)
URL: http://localhost:8080/oauth/authorize?client_id=oauth_client&scope=read&response_type=code&state=rensanning&redirect_uri=http://default-oauth-callback.com
由于对/oauth/authorize开启了HTTP Basic认证,所以需要输入密码:

输入正确用户名密码(user/123)后显示授权页:

选择Approve点击Authorize按钮后,自动跳转到 http://default-oauth-callback.com?code=sdb6vF&state=rensanning

URL中的code参数值即为授权码,该值是一个6位英数字的随机数,具体可以看源码:org.springframework.security.oauth2.common.util.RandomValueStringGenerator.generate()
!!授权码10分钟过期目前还没有实现!! https://github.com/spring-projects/spring-security-oauth/issues/725
(2)获得令牌(Post)
URL: http://localhost:8080/oauth/token?grant_type=authorization_code&redirect_uri=http://default-oauth-callback.com&code=sdb6vF
【/oauth/token】默认采用的是HTTP Basic Auth(org.springframework.security.web.authentication.www.BasicAuthenticationFilter),所以需要在HTTP的header里提供clientId和clientSecret的Base64值。具体可以执行Test.java获取。
Authorization: Basic b2F1dGhfY2xpZW50Om9hdXRoX2NsaWVudF9zZWNyZXQ=

通过以下设置,可以通过参数的形式传递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(); } }
返回令牌信息
引用
{
"access_token": "1cc5ffbd-faac-4d20-afd9-b8531acd248e",
"token_type": "bearer",
"refresh_token": "5b319fed-5600-4ea2-8c4f-61f6e3ea6e41",
"expires_in": 1631,
"scope": "read"
}
"access_token": "1cc5ffbd-faac-4d20-afd9-b8531acd248e",
"token_type": "bearer",
"refresh_token": "5b319fed-5600-4ea2-8c4f-61f6e3ea6e41",
"expires_in": 1631,
"scope": "read"
}
生成的令牌是一个UUID,具体可以看源码:org.springframework.security.oauth2.provider.token.DefaultTokenServices.createAccessToken()
(4)访问API(Get)
直接访问API返回401。URL: http://localhost:8080/api/get

通过access_token参数访问。URL: http://localhost:8080/api/get?access_token=1cc5ffbd-faac-4d20-afd9-b8531acd248e

通过http header参数访问。URL: http://localhost:8080/api/get
Authorization: Bearer 1cc5ffbd-faac-4d20-afd9-b8531acd248e

*** @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
(5)刷新令牌(Post)
URL: http://localhost:8080/oauth/token?grant_type=refresh_token&refresh_token=5b319fed-5600-4ea2-8c4f-61f6e3ea6e41
Authorization: Basic b2F1dGhfY2xpZW50Om9hdXRoX2NsaWVudF9zZWNyZXQ=
返回新的access_token:
引用
{
"access_token": "3cbe70fc-753f-44ff-9bb4-0ba6bc3c9aab",
"token_type": "bearer",
"refresh_token": "5b319fed-5600-4ea2-8c4f-61f6e3ea6e41",
"expires_in": 1800,
"scope": "read"
}
"access_token": "3cbe70fc-753f-44ff-9bb4-0ba6bc3c9aab",
"token_type": "bearer",
"refresh_token": "5b319fed-5600-4ea2-8c4f-61f6e3ea6e41",
"expires_in": 1800,
"scope": "read"
}

旧的Token就不能再用了:

通过新的Token访问API:

同理,可以测试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