关于了解Oauth2.0是什么,阮一峰老师的http://www.ruanyifeng.com/blog/2014/05/oauth_2_0.html已经详细地分析了Oauth2.0。在Spring Boot+Spring Cloud的微服务架构中,如果每个服务都维护一套自己的鉴权逻辑,这样不同的服务之间可能无法协同运作,因此出现了Spring Security Oauth2.0:通过鉴权服务提供acess_token,其它微服务作为资源服务通过acess_token调用受保护的接口。本文中鉴权服务器和资源服务器是在一起的,首先通过自制的登录页面进行登录,登录成功后再通过Spring Security的组件预留的api进行令牌模式或密码模式获取acess_token,通过携带acess_token调用受保护的接口。
常用的2种授权模式
授权码模式,首先要经过授权获取对应的授权code,然后再通过token接口获取access_token,用于第三方去请求某个接口的时候多用这种模式,如微信的API调用。
密码模式,直接通过密码和用户名token接口获取access_token,在我负责过的功能当中,一般该模式适用于内部服务之间的调用。
Pom.xml依赖
org.springframework.cloud
spring-cloud-starter-oauth2
org.springframework.boot
spring-boot-starter-thymeleaf
添加application.yml设置登录静态页面的位置
spring:
thymeleaf:
prefix: classpath:/templates/
该配置类添加@EnableAuthorizationServer注解使该模块服务成为一个鉴权服务,他提供了/oauth/authorize,/oauth/token,/oauth/check_token,/oauth/confirm_access,/oauth/error,token相关的预设API接口,其中在configure方法中.inMemory()设置该了该服务的相关信息保存在浏览器内存中,.withClient("demoApp")设置了该鉴权服务的id,.secret("demoAppSecret")设置了鉴权密码,.authorizedGrantTypes("authorization_code", "password","refresh_token") 设置了服务支持的授权方法
package com.security.oauth2test.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer;
@Configuration
@EnableAuthorizationServer //提供/oauth/authorize,/oauth/token,/oauth/check_token,/oauth/confirm_access,/oauth/error
public class OAuth2ServerConfig extends AuthorizationServerConfigurerAdapter {
@Override
public void configure(AuthorizationServerSecurityConfigurer oauthServer) throws Exception {
oauthServer
.tokenKeyAccess("permitAll()")
.checkTokenAccess("isAuthenticated()")
.allowFormAuthenticationForClients();
}
@Autowired
private AuthenticationManager authenticationManager;
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints.authenticationManager(authenticationManager);
}
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory()//数据存在内存中
.withClient("demoApp")//授权服务器id
.secret("demoAppSecret")//授权密码
.authorizedGrantTypes("authorization_code", "password", "refresh_token")//获取模式
.scopes("all")
.resourceIds("oauth2-resource")//资源服务器id
.accessTokenValiditySeconds(1200)//token的存在时间
.refreshTokenValiditySeconds(50000);//刷新token的token的存在时间
}
}
无论是授权码模式还是密码模式,首先要登录系统,形成登录成功的浏览器SESSION,下面源码分析会解析为什么需要登录才能继续进行token的获取。
授权码模式
client_id:这是客户端id,在本文中在配置类中设置了该值为demoApp
response_type: 表示授权模式,在授权码模式中,该值为code
redirect_uri:需要跳转的url
在授权码模式中,首先要向客户端进行请求授权,当我们带上参数访问/oauth/authorize并且点击授权后就能获取授权的code值,本文中该值为
olfUVS
client_id:在本文中在配置类中设置了该值为demoApp
client_secret:在本文中在配置类中定义为demoAppSecret
code:上个url反馈的值
grant_type:使用授权码模式,故这个值为authorization_code
redirect_uri:需要跳转的url
获得授权之后,可以通过code值,和预先约定的信息,通过访问/oauth/token获取访问资源的access_token,要注意的是使用授权码
模式进行token获取的时候跳转的url必须两次请求的时候都是一样的。
密码模式
client_id:在本文中在配置类中设置了该值为demoApp
client_secret:在本文中在配置类中定义为demoAppSecret
grant_type:使用密码模式,故这个值为password
username:获取token的用户名
password:获取token的用户对应的密码
无论是授权码模式还是密码模式,当他们访问端口生成token,使用返回的token:e701a724-a55a-4c6c-8cd2-0498c98d409e 成功调用的接口
无效的token调用失败
5.源码分析
在令牌模式和密码模式的源码中,!(principal instanceof Authentication)要求了用户必须进行登录才能进行操作,反过来看因为如果没有通过身份认证,就不能通过该身份生成对应的access_token
@RequestMapping(value = "/oauth/token", method=RequestMethod.POST)
public ResponseEntity postAccessToken(Principal principal, @RequestParam
Map parameters) throws HttpRequestMethodNotSupportedException {
if (!(principal instanceof Authentication)) {
throw new InsufficientAuthenticationException(
"There is no client authentication. Try adding an appropriate authentication filter.");
}
部分省略........
}
@RequestMapping(value = "/oauth/authorize")
public ModelAndView authorize(Map model, @RequestParam Map parameters,
SessionStatus sessionStatus, Principal principal) {
try {
if (!(principal instanceof Authentication) || !((Authentication) principal).isAuthenticated()) {
throw new InsufficientAuthenticationException(
"User must be authenticated with Spring Security before authorization can be completed.");
}
部分省略........
}
在TokenEndpoint中,token获取接口/oauth/token是写有GET和POST方法的,但是在默认的配置下只允许使用POST做请求方式。
@RequestMapping(value = "/oauth/token", method=RequestMethod.GET)
public ResponseEntity getAccessToken(Principal principal, @RequestParam
Map parameters) throws HttpRequestMethodNotSupportedException {
if (!allowedRequestMethods.contains(HttpMethod.GET)) {
throw new HttpRequestMethodNotSupportedException("GET");
}
return postAccessToken(principal, parameters);
}
@RequestMapping(value = "/oauth/token", method=RequestMethod.POST)
public ResponseEntity postAccessToken(Principal principal, @RequestParam
Map parameters) throws HttpRequestMethodNotSupportedException {}
如果需要让/
oauth/token
支持GET方法就必须在初始化的时候给它多添加请求的方法方式,只需要在allowedTokenEndpointRequestMethods方法中多添加请求类型就可以了
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints.authenticationManager(customAuthenticationManager());
endpoints.allowedTokenEndpointRequestMethods(HttpMethod.GET,HttpMethod.POST);
}
github:https://github.com/tale2009/spring-security-learning/tree/master/oauth2
到这里,关于在分布式系统上使用Spring Security的前期知识已经通过三篇博文讲完,基于Spring家族的关系,Spring Security变得非常契合用在Spring Boot+Spring Cloud架构下,后面我会发布关于Spring Boot+Spring Cloud架构下如何使用Spring Security的文章。