OAuth2授权码模式
授权码模式
授权流程
- 用户访问客户端,后者将前者导向认证服务器
- 用户选择是否给予客户端授权
- 假设用户给予授权,认证服务器将用户导向客户端事先指定的重定向
URI
,同时附上一个授权码
- 客户端收到授权码,附上早先的重定向
URI
,向认证服务器申请令牌。这一步是在客户端的后台的服务器上完成的,对用户不可见
- 认证服务器核对了授权码和重定向
URI
,确认无误后,向客户端发送访问令牌(access token
)和更新令牌(refresh token
)等
使用场景
- 授权码模式是最常见的一种授权模式,在oauth2.0内是最安全和最完善的。
- 适用于所有有Server端的应用,如Web站点、有Server端的手机客户端。
- 可以得到较长期限授权。
图解
- 授权码模式是最常见常用的模式,我们所熟悉的微博,QQ 等都是这种模式。
- 另外也是最繁琐的一种方式,如果弄懂了这个相信接下来的三种类型都会迎刃而解。
- 这种模式和其他最大的区别就在于是否有
授权码
这个步骤。
授权码示例
授权服务器
创建auth-server服务
引入POM文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-oauth2</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-security</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-config</artifactId> </dependency>
|
配置文件
注意使用了配置中心,eureka配置存放在配置中心
application
创建application.properties
1 2
| server.port=9988 spring.application.name=auth-server
|
bootstrap
创建bootstrap.properties
1 2 3 4 5 6 7 8 9 10
| spring.cloud.config.uri=http://192.168.64.128:8899
spring.cloud.config.label=master
spring.cloud.config.name=application
spring.cloud.config.profile=dev
spring.cloud.config.fail-fast=true
|
启动类
1 2 3 4 5 6 7 8
| @SpringBootApplication @EnableEurekaClient public class AuthApplication {
public static void main(String[] args) { SpringApplication.run(AuthApplication.class); } }
|
授权服务配置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75
|
@Configuration @EnableAuthorizationServer public class AuthorizationServerConfiguration extends AuthorizationServerConfigurerAdapter {
@Autowired private AuthenticationManager authenticationManager;
@Autowired UserDetailsService userDetailsService;
@Bean public TokenStore memoryTokenStore() { return new InMemoryTokenStore(); }
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception { clients.inMemory() .withClient("order-server") .authorizedGrantTypes("authorization_code", "refresh_token") .resourceIds("order-server") .authorities("authorization_code") .redirectUris("http://baidu.com") .scopes("test") .secret(PasswordEncoderFactories.createDelegatingPasswordEncoder().encode("123456")) .accessTokenValiditySeconds(1200) .refreshTokenValiditySeconds(50000);
}
@Override public void configure(AuthorizationServerSecurityConfigurer security) throws Exception { security.tokenKeyAccess("permitAll()").checkTokenAccess("isAuthenticated()").allowFormAuthenticationForClients(); }
@Override public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception { endpoints.authenticationManager(authenticationManager).tokenStore(memoryTokenStore()).userDetailsService(userDetailsService).allowedTokenEndpointRequestMethods(HttpMethod.GET, HttpMethod.POST); } }
|
spring security配置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
|
@EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Bean @Override public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); }
@Bean @Override protected UserDetailsService userDetailsService() { InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager(); manager.createUser(User.withUsername("admin").password(PasswordEncoderFactories.createDelegatingPasswordEncoder().encode("admin")).authorities("USER").build()); return manager; }
@Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(userDetailsService()); } }
|
启动测试
获取授权码
在有了服务提供商之后,我们就可以根据OAuth的规则,来要求用户给予授权,所以这里需要第三方应用去
请求参数
参数 |
描述 |
localhost |
8080这里是我服务的地址以及端口,根据每个人的情况是不同的 |
/oauth/authorize |
这个是Spring Security OAuth2默认提供的接口 |
response_type |
表示授权类型,必选项,此处的值固定为”code” |
client_id |
表示客户端的ID,必选项。这里使用的是项目启动时,控制台输出的security.oauth2.client.clientId,当然该值可以在配置文件中自定义 |
redirect_uri |
表示重定向URI,可选项。即用户授权成功后,会跳转的地方,通常是第三方应用自己的地址 |
scope |
表示申请的权限范围,可选项。这一项用于服务提供商区分提供哪些服务数据 |
state |
表示客户端的当前状态,可以指定任意值,认证服务器会原封不动地返回这个值。这里没有使用到该值 |
身份验证
1
| http://127.0.0.1:9988/oauth/authorize?response_type=code&client_id=order-server&redirect_uri=http://baidu.com
|
这里我们访问到接口后,会出现如下的界面
如果没有登录,浏览器会重定向到登录界面
该界面主要是用于用户登录的,不然怎么知道想要哪个用户的数据呢?
服务授权
输入用户名和密码(admin/admin)点击登录,这时会进入授权页
这里就是要求用户授权的界面了,有点类似于我们使用QQ进行第三方登录时候的界面。上面写有了是哪一个第三方应用需要哪些数据,我们这里就点确认授权,这里就会根据配置的redirect_uri
进行跳转,并且是带有一个参数的。
点击授权,浏览器会从定向到回调地址上,携带code
参数
授权码是 sNMfYR
这个code就是下一步第三方应用向服务器申请令牌使用的
获取令牌
这里我们拿着上一步获取到的code,以及项目初始化时打印的clientId和secret去获取Token,这里需要使用POST方法。
请求参数
请求的Header中有一个Authorization参数,该参数的值是Basic + (clientId:secret Base64值)
参数 |
描述 |
grant_type |
表示使用的授权模式,必选项,此处的值固定为”authorization_code”。 |
code |
表示上一步获得的授权码,必选项。 |
redirect_uri |
表示重定向URI,必选项,且必须与A步骤中的该参数值保持一致。 |
client_id |
表示客户端ID,必选项。 |
获取Token
1
| http://localhost:9988/oauth/token?grant_type=authorization_code&code=sNMfYR&client_id=order-server&client_secret=123456&redirect_uri=http://baidu.com
|
如果请求成功,就可以顺利的拿到Token
返回数据格式如下
1 2 3 4 5 6 7
| { "access_token": "353f4d37-2668-4f0d-b7a7-ded3350258d0", "token_type": "bearer", "refresh_token": "bd73e4f0-08f8-456c-b96f-4019aa0ad2e7", "expires_in": 1199, "scope": "test" }
|
返回参数
参数 |
描述 |
access_token |
表示访问令牌,必选项 |
token_type |
表示令牌类型,该值大小写不敏感,必选项,可以是bearer类型或mac类型。 |
expires_in |
表示过期时间,单位为秒。如果省略该参数,必须其他方式设置过期时间。 |
refresh_token |
表示更新令牌,用来获取下一次的访问令牌,可选项。 |
scope |
表示权限范围,如果与客户端申请的范围一致,此项可省略。 |
再次请求发现错误了,这个授权码只能使用一次,如想要使用还需要从头继续申请
校验授权码
请求所需参数:token
1
| http://127.0.0.1:9988/oauth/check_token?token=e3fa343f-11b7-4c44-9ece-af7c7c43cb49
|
调用发现出现401 错误,没有权限,这个时候需要进行baseAuth认证
其中 username是 client_id,密码是配置的 secret
配置完成再次访问
返回如下信息校验成功
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| { "aud": [ "order-server" ], "user_name": "admin", "scope": [ "test" ], "active": true, "exp": 1590648560, "authorities": [ "USER" ], "client_id": "order-server" }
|
我们故意把token改错试试
刷新token
请求所需参数:grant_type、refresh_token、client_id、client_secret
其中grant_type为固定值:grant_type=refresh_token
1
| http://localhost:9988/oauth/token?grant_type=refresh_token&refresh_token=ac8a2ff7-be4b-4d22-8dc6-93ddbfc9d5ce&client_id=order-server&client_secret=123456
|
资源服务器
引入POM文件
1 2 3 4 5 6
| security.oauth2.resource.token-info-uri=http://127.0.0.1:9988/oauth/check_token security.oauth2.client.clientId=order-server security.oauth2.client.user-authorization-uri=http://127.0.0.1:9988/oauth/authorize security.oauth2.client.client-secret=123456 security.oauth2.client.access-token-uri=http://127.0.0.1:9988/oauth/token security.oauth2.client.scope=test
|
启动类配置
增加@EnableResourceServer注解
1 2 3 4 5 6 7 8 9 10 11 12
| @SpringBootApplication @EnableEurekaClient
@EnableHystrix
@EnableResourceServer public class OrderApplication {
public static void main(String[] args) { SpringApplication.run(OrderApplication.class); } }
|
测试
访问测试接口
http://127.0.0.1:8083/order/createorder/1?access_token=b8a3d75a-a3d7-4ebc-9f1d-e880d8a48e27
测试成功
将token改错误
增加redis存储
授权服务器配置
引入POM
加入redis相关jar
1 2 3 4
| <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency>
|
配置文件
application.properties增加redis配置
1 2 3 4
| spring.redis.host=192.168.64.128 spring.redis.port:6379 spring.redis.database:0
|
授权服务配置
授权配置类加入Redis配置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118
|
@Configuration @EnableAuthorizationServer public class AuthorizationServerConfiguration extends AuthorizationServerConfigurerAdapter {
@Autowired public RedisConnectionFactory redisConnectionFactory;
@Autowired private AuthenticationManager authenticationManager;
@Autowired UserDetailsService userDetailsService;
@Bean public TokenStore memoryTokenStore() { return new InMemoryTokenStore(); }
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception { clients.inMemory() .withClient("order-server") .authorizedGrantTypes("authorization_code", "refresh_token") .resourceIds("order-server") .authorities("authorization_code") .redirectUris("http://baidu.com") .scopes("test") .secret(PasswordEncoderFactories.createDelegatingPasswordEncoder().encode("123456")) .accessTokenValiditySeconds(1200) .refreshTokenValiditySeconds(50000);
}
@Override public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
security.allowFormAuthenticationForClients(). tokenKeyAccess("permitAll()"). checkTokenAccess("isAuthenticated()"); }
@Override public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception { endpoints.authenticationManager(authenticationManager). userDetailsService(userDetailsService). allowedTokenEndpointRequestMethods(HttpMethod.GET, HttpMethod.POST). tokenServices(tokenService()). tokenStore(tokenStore()); }
@Bean public TokenStore tokenStore() { RedisTokenStore redisTokenStore = new RedisTokenStore(redisConnectionFactory); redisTokenStore.setPrefix("auth-token:"); return redisTokenStore; }
@Bean public DefaultTokenServices tokenService() { DefaultTokenServices tokenServices = new DefaultTokenServices(); tokenServices.setTokenStore(tokenStore()); tokenServices.setSupportRefreshToken(true); tokenServices.setReuseRefreshToken(true); tokenServices.setAccessTokenValiditySeconds(12 * 60 * 60); tokenServices.setRefreshTokenValiditySeconds(7 * 24 * 60 * 60); return tokenServices; } }
|
资源服务器
引入POM
加入redis相关jar
1 2 3 4
| <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency>
|
配置文件
application.properties增加redis配置
1 2 3 4
| spring.redis.host=192.168.64.128 spring.redis.port:6379 spring.redis.database:0
|
资源服务器配置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| @Configuration
@EnableResourceServer public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
@Autowired private RedisConnectionFactory redisConnectionFactory;
@Override public void configure(ResourceServerSecurityConfigurer resources) throws Exception { resources.stateless(true); resources.tokenStore(tokenStore()).resourceId("order-server"); }
@Bean public RedisTokenStore tokenStore() { RedisTokenStore redisTokenStore = new RedisTokenStore(redisConnectionFactory); redisTokenStore.setPrefix("auth-token:"); return redisTokenStore; } }
|
自动续签
主要思路
- 首先用过期token访问受拦截资源
- 认证失败返回401的时候调用异常处理器
- 通过异常处理器结合refresh_token进行token的刷新
- 刷新成功则通过请求转发(request.getRequestDispatcher)的方式再次访问受拦截资源
OAuth2AuthenticationProcessingFilter
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
final boolean debug = logger.isDebugEnabled(); final HttpServletRequest request = (HttpServletRequest) req; final HttpServletResponse response = (HttpServletResponse) res;
try {
Authentication authentication = tokenExtractor.extract(request);
...
catch (OAuth2Exception failed) { SecurityContextHolder.clearContext();
if (debug) { logger.debug("Authentication request failed: " + failed); } eventPublisher.publishAuthenticationFailure(new BadCredentialsException(failed.getMessage(), failed), new PreAuthenticatedAuthenticationToken("access-token", "N/A"));
authenticationEntryPoint.commence(request, response, new InsufficientAuthenticationException(failed.getMessage(), failed));
return; }
chain.doFilter(request, response); }
|
分析默认端点异常处理器
从过滤器源码中我们可以看到此异常处理器是有默认实现类的
1 2 3 4 5 6 7 8 9
| public class OAuth2AuthenticationProcessingFilter implements Filter, InitializingBean {
private final static Log logger = LogFactory.getLog(OAuth2AuthenticationProcessingFilter.class);
private AuthenticationEntryPoint authenticationEntryPoint = new OAuth2AuthenticationEntryPoint();
...
}
|
通过查看此默认处理器,我们可以发现里面主要调用了doHandle的方法
1 2 3 4 5 6 7 8 9 10 11 12 13
| public class OAuth2AuthenticationEntryPoint extends AbstractOAuth2SecurityExceptionHandler implements AuthenticationEntryPoint { ... public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException { doHandle(request, response, authException); } ... }
|
我们再次查看doHandle的具体内容可以得出此过滤器的主要功能有3个:
- 解析异常类型
- 扩展respone的一些属性和内容
- respone 刷新缓存直接返回
1 2 3 4 5 6 7 8 9 10 11
| protected final void doHandle(HttpServletRequest request, HttpServletResponse response, Exception authException) throws IOException, ServletException { try { ResponseEntity<?> result = exceptionTranslator.translate(authException); result = enhanceResponse(result, authException); exceptionRenderer.handleHttpEntityResponse(result, new ServletWebRequest(request, response)); response.flushBuffer(); }
... }
|
重写异常处理器
对默认异常处理器的分析,我们可以得出如果是我们需要的异常(401异常)则用我们自定义的方法进行处理,如果是其他异常则让原来的异常处理器处理即可,大致思路如下:
- 通过exceptionTranslator.translate(authException)解析异常,判断异常类型(status)
- 如果不是401异常,则直接调用默认异常处理器的处理方法即可
- 如果是401异常则向授权服务器发起token刷新的请求
- 如果token刷新成功,则通过request.getRequestDispatcher(request.getRequestURI()).forward(request,response);再次请求资源
- 如果token刷新失败,要么跳转到登陆页面(web的话也可以通过response.sendirect跳转到登陆页面),要么返回错误信息(json)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58
| public class LLGAuthenticationEntryPoint extends OAuth2AuthenticationEntryPoint { @Autowired private OAuth2ClientProperties oAuth2ClientProperties; @Autowired private BaseOAuth2ProtectedResourceDetails baseOAuth2ProtectedResourceDetails; private WebResponseExceptionTranslator<?> exceptionTranslator = new DefaultWebResponseExceptionTranslator(); @Autowired RestTemplate restTemplate; @Override public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException { try { ResponseEntity<?> result = exceptionTranslator.translate(authException); if (result.getStatusCode() == HttpStatus.UNAUTHORIZED) { MultiValueMap<String, String> formData = new LinkedMultiValueMap<String, String>(); formData.add("client_id", oAuth2ClientProperties.getClientId()); formData.add("client_secret", oAuth2ClientProperties.getClientSecret()); formData.add("grant_type", "refresh_token"); Cookie[] cookie=request.getCookies(); for(Cookie coo:cookie){ if(coo.getName().equals("refresh_token")){ formData.add("refresh_token", coo.getValue()); } } HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); Map map = restTemplate.exchange(baseOAuth2ProtectedResourceDetails.getAccessTokenUri(), HttpMethod.POST, new HttpEntity<MultiValueMap<String, String>>(formData, headers), Map.class).getBody(); if(map.get("error")!=null){ response.setStatus(401); response.setHeader("Content-Type", "application/json;charset=utf-8"); response.getWriter().print("{\"code\":1,\"message\":\""+map.get("error_description")+"\"}"); response.getWriter().flush(); }else{ for(Object key:map.keySet()){ response.addCookie(new Cookie(key.toString(),map.get(key).toString())); } request.getRequestDispatcher(request.getRequestURI()).forward(request,response); } }else{ super.commence(request,response,authException); } } catch (Exception e) { e.printStackTrace(); } } }
|
将处理器设置到过滤器上
由于spring security遵循适配器的设计模式,所以我们可以直接从配置类上配置此处理器
1 2 3 4 5 6 7 8 9 10 11 12 13
| @EnableResourceServer @EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true) public abstract class ResServerConfig extends ResourceServerConfigurerAdapter { ...
@Override public void configure(ResourceServerSecurityConfigurer resources) throws Exception { super.configure(resources); resources.authenticationEntryPoint(new LLGAuthenticationEntryPoint()); }
|