之前的文章简单介绍了Spring MVC + Spring security OAuth2的简单配置方法,主要以XML进行配置。这一篇将使用Spring boot做一个更全面的说明,以Java和yml来进行配置~
虽然配置方式与之前相比有些不同,但是运作流程还是一样,依然是Filter chain进行各种顾虑和验证操作~
OAuth2验证服务器的主要功能包括:
1、验证用户的username和password;
2、验证客户端的client_id和secret;
3、为通过验证的用户或者客户端颁发code或者token;
4、验证客户端提供的token是否合法有效;
5、根据有效的token提供对应的用户或者客户端详细信息;
plugins {
id 'java'
}
group 'com.chenlei'
version '1.0-SNAPSHOT'
sourceCompatibility = 1.8
repositories {
mavenCentral()
}
dependencies {
compile group: 'org.springframework.boot', name: 'spring-boot-starter-web', version: '2.0.3.RELEASE'
compile group: 'org.springframework.boot', name: 'spring-boot-starter-security', version: '2.0.3.RELEASE'
compile group: 'org.springframework.security.oauth.boot', name: 'spring-security-oauth2-autoconfigure', version: '2.0.1.RELEASE'
testCompile group: 'junit', name: 'junit', version: '4.12'
}
spring-security-oauth2-autoconfigure将为我们省去很多的配置事项
@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
}
通过
@EnableAuthorizationServer
来启用AuthorizationServer的默认实现,以提供/oauth/token、/oauth/check_token、/oauth/authorize等endpoint
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private PasswordEncoder passwordEncoder;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication().withUser("admin").password(passwordEncoder.encode("admin")).authorities("IS_AUTHENTICATED_FULLY");
}
}
在authorization_code模式下,通过Spring security来进行用户认证,SSO将通过此模式实现
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory().withClient("client-1")
.secret(this.passwordEncoder.encode("client"))
.authorizedGrantTypes("authorization_code")
.scopes("read")
.redirectUris("http://localhost:8081")
.autoApprove(true)
.and()
.withClient("client-2")
.secret(this.passwordEncoder.encode("client"))
.authorizedGrantTypes("authorization_code")
.scopes("read")
.redirectUris("http://localhost:8082")
.autoApprove(true)
.and()
.withClient("client-3")
.secret(this.passwordEncoder.encode("client"))
.authorizedGrantTypes("client_credentials")
.scopes("rea.*")
.and()
.withClient("client-4")
.secret(this.passwordEncoder.encode("client"))
.authorizedGrantTypes("client_credentials")
.scopes("read")
.and()
.withClient("client-5")
.secret(this.passwordEncoder.encode("client"))
.authorizedGrantTypes("client_credentials")
.scopes("read");
}
这里先配置五个不同类型不同权限的客户端,客户端可能是资源服务器,也可能就是普通的应用程序
@RestController
public class UserInfoController {
@RequestMapping("/oauth/me")
public Principal userInfo (Principal principal) {
return principal;
}
}
一个普通的REST API,为客户端提供用户详细信息以判断用户是否登陆、是否有权限访问资源
@Configuration
@EnableResourceServer
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
@Override
public void configure(HttpSecurity http) throws Exception {
http.requestMatchers().antMatchers("/oauth/me")
.and().authorizeRequests().anyRequest().access("#oauth2.hasScope('read')")
.and().anonymous().disable();
}
}
/oauth/me本身也是一种资源,只有提供合法token的client才能访问/oauth/me
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().anyRequest().authenticated() // 所有请求都需要授权
.and().formLogin() // 使用默认的登陆界面
.and().anonymous().disable();
}
SSO的基本流程:
SSO客户端配置:
server:
port: 8081
servlet:
context-path: /client-1
security:
oauth2:
client:
client-id: client-1
client-secret: client
access-token-uri: http://localhost:8080/server/oauth/token
user-authorization-uri: http://localhost:8080/server/oauth/authorize
resource:
user-info-uri: http://localhost:8080/server/oauth/me
logging:
level:
org.springframework.web: DEBUG
org.springframework.security: DEBUG
@Configuration
@EnableOAuth2Sso
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().anyRequest().authenticated(); // 所有请求都需要验证
}
}
@EnableOAuth2Sso启用SSO客户端配置,即拦截/login的filter chain
第三方应用访问资源服务器受保护资源时需要提供自身的token、资源服务器根据自己的client_id和secret登陆认证服务器并验证第三方应用token的token是否有效,从而确定是否在资源服务器保存第三方应用的认证信息
server:
port: 8082
servlet:
context-path: /client-2
security:
oauth2:
client:
client-id: client-2
client-secret: client
access-token-uri: http://localhost:8080/server/oauth/token
user-authorization-uri: http://localhost:8080/server/oauth/authorize
resource:
user-info-uri: http://localhost:8080/server/oauth/me
authorization:
check-token-access: http://localhost:8080/server/oauth/check_token
logging:
level:
org.springframework.web: DEBUG
org.springframework.security: DEBUG
@Override
public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
RemoteTokenServices remoteTokenServices = new RemoteTokenServices();
remoteTokenServices.setCheckTokenEndpointUrl(this.getAuthorizationServerProperties().getCheckTokenAccess());
remoteTokenServices.setClientId(this.oAuth2ClientProperties.getClientId());
remoteTokenServices.setClientSecret(this.oAuth2ClientProperties.getClientSecret());
resources.tokenServices(remoteTokenServices);
}
@Override
public void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().anyRequest().access("#oauth2.hasScope('read')")
.and().anonymous().disable();
}
第三方应用程序通过oAuth2RestTemplate访问资源服务器通过,oAuth2RestTemplate需要绑定client_id、secret以及认证服务器信息,从而获得对应的token(权限)
server:
port: 8083
servlet:
context-path: /client-3
security:
oauth2:
client:
client-id: client-3
client-secret: client
access-token-uri: http://localhost:8080/server/oauth/token
user-authorization-uri: http://localhost:8080/server/oauth/authorize
grant-type: client_credentials
logging:
level:
org.springframework.web: DEBUG
org.springframework.security: DEBUG
com.chenlei: DEBUG
@Configuration
public class AuthorizationClientConfig {
@Bean
@ConfigurationProperties(prefix = "security.oauth2.client")
public ClientCredentialsResourceDetails getClientCredentialsResourceDetails() {
return new ClientCredentialsResourceDetails();
}
@Bean
public OAuth2RestOperations oAuth2RestOperations() {
return new OAuth2RestTemplate(getClientCredentialsResourceDetails());
}
}
@RestController
@RequestMapping("/resource")
public class ResourceController {
private final Log logger = LogFactory.getLog(getClass());
@Autowired
private OAuth2RestOperations oAuth2RestTemplate;
@RequestMapping("/")
public String getResource() {
logger.debug("I am client-3 and I will call client-4...");
logger.debug("The client-4 say: " + oAuth2RestTemplate.getForObject("http://localhost:8084/client-4/resource/", String.class));
logger.debug("The client-4 say: " + oAuth2RestTemplate.getForObject("http://localhost:8084/client-4/resource/web", String.class));
logger.debug("The client-4 say: " + oAuth2RestTemplate.getForObject("http://localhost:8084/client-4/resource/method", String.class));
logger.debug("The client-4 say: " + oAuth2RestTemplate.getForObject("http://localhost:8084/client-4/resource/cse/web", String.class));
logger.debug("The client-4 say: " + oAuth2RestTemplate.getForObject("http://localhost:8084/client-4/resource/cse/method", String.class));
return "I am client-3!!!";
}
@RequestMapping("/call/2")
public String callClient_2() {
logger.debug("I am client-3 and I will call client-2...");
logger.debug("The client-2 say: " + oAuth2RestTemplate.getForObject("http://localhost:8082/client-2/resource/", String.class));
return "I am client-3!!!";
}
}
OAuth默认的OAuth2SecurityExpressionMethods提供一些常用的鉴权方法,特殊情况下我们需要扩展自己的鉴权方法,或者直接调用自己的外部方法。
Spring security提供两种方式配置权限拦截,一种是拦截URL并配置访问权限,称之为WebSecurity,另一种是在方法上加注解,控制方法访问权限,称之为MethodSecurity
@Override
public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
RemoteTokenServices remoteTokenServices = new RemoteTokenServices();
remoteTokenServices.setCheckTokenEndpointUrl(this.getAuthorizationServerProperties().getCheckTokenAccess());
remoteTokenServices.setClientId(this.oAuth2ClientProperties.getClientId());
remoteTokenServices.setClientSecret(this.oAuth2ClientProperties.getClientSecret());
ExtendOAuth2WebSecurityExpressionHandler extendOAuth2WebSecurityExpressionHandler = new ExtendOAuth2WebSecurityExpressionHandler();
// 往ExpressionHandler中设置ApplicationContext,否则在鉴权时无法通过@调用CustomSecurityExpression,比如:@cse.permitAll(authentication, 'customSecurityExpression')
extendOAuth2WebSecurityExpressionHandler.setApplicationContext(this.applicationContext);
resources.tokenServices(remoteTokenServices).expressionHandler(extendOAuth2WebSecurityExpressionHandler);
}
@Override
public void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().antMatchers("/resource/web/**").access("#oauth2.hasMatchingScope('read')") // 使用扩展的表达式鉴权
.antMatchers("/resource/cse/web/**").access("@cse.permitAll(authentication, 'customSecurityExpression')") // 使用自定义表达式鉴权
.and().anonymous().disable();
}
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class GlobalMethodSecurityConfig extends GlobalMethodSecurityConfiguration {
@Autowired
private ApplicationContext applicationContext;
@Override
protected MethodSecurityExpressionHandler createExpressionHandler() {
ExtendOAuth2MethodSecurityExpressionHandler extendOAuth2MethodSecurityExpressionHandler = new ExtendOAuth2MethodSecurityExpressionHandler();
// 往ExpressionHandler中设置ApplicationContext,否则在鉴权时无法通过@调用CustomSecurityExpression,比如:@cse.permitAll(authentication, 'customSecurityExpression')
extendOAuth2MethodSecurityExpressionHandler.setApplicationContext(this.applicationContext);
return extendOAuth2MethodSecurityExpressionHandler;
}
}
Json web token是认证服务器使用私钥将用户详细信息签名加密参数的token,客户端可以访问认证服务器提供的endpoint获得对应的公钥,然后用公钥将jwt解密,从而获得对应的用户信息,省去了客户端频繁用token换取用户详细信息的过程,从而提高效率,减轻认证服务器的负担。
LiondeMBP:tmp lion$ keytool -genkeypair -alias jwt -keyalg RSA -dname "CN=jwt, L=Wuhan, S=Hubei, C=CN" -keypass awesomePass -keystore jwt.jks -storepass awesomePass
server:
port: 8080
servlet:
context-path: /server
logging:
level:
org.springframework.web: DEBUG
org.springframework.security: DEBUG
org.springframework.boot.autoconfigure: DEBUG
keystore:
password: awesomePass
@Bean
public TokenStore tokenStore() {
return new JwtTokenStore(jwtTokenEnhancer());
}
@Bean
public JwtAccessTokenConverter jwtTokenEnhancer() {
String password = this.environment.getProperty("keystore.password");
KeyStoreKeyFactory keyStoreKeyFactory = new KeyStoreKeyFactory(new ClassPathResource("jwt.jks"), password.toCharArray());
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
converter.setKeyPair(keyStoreKeyFactory.getKeyPair("jwt"));
return converter;
}
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints.tokenStore(this.tokenStore())
.tokenEnhancer(jwtTokenEnhancer())
.userApprovalHandler(this.userApprovalHandler());
}