使用 OAuth2 通过 spring 引导使用 authorization_code 授权和 JWT 登录客户端
Use OAuth2 to sign in to client with spring boot using authorization_code grant and JWT
我正在尝试使用 JWT 和 OAuth2 在 server/client 上获得单一登录。
除了用户名(我打算将其嵌入到 jwt 中)之外,我没有其他资源可以从客户端外部请求。
我的服务器正在运行并且可以很好地处理 jwt 令牌(使用邮递员的 oauth2 令牌身份验证进行测试)。我只是不知道如何处理客户。
我需要让我的客户端成为 ResourceServer 吗?或者我应该直接使用@EnableOAuth2Sso 教程吗?
对于我的客户,我有以下规格:
- 我想使用 "authorization_code" 授权类型
- 我想把客户端的所有页面隐藏在oauth2登录后面
- 用户请求一个页面,如果他们没有登录(没有带有有效 jwt 令牌的身份验证 header),他们应该被重定向到 authserver/oauth/authorize 路径
- 显示登录表单,用户使用用户名密码登录
- 如果登录正确,他们将被重定向回请求的客户端页面
- 如果用户在 JWT 过期之前返回,则他们不必再次登录。
- 我想使用 Spring 安全性,自定义代码越少越好。
我认为这是可能的,并且正是单点登录应该做的。
我的第一个想法是使用 @EnableOAuth2Sso 但这不能正常工作:
- 我被重定向到服务器上的登录页面
- 我正确登录
- 我被重定向到:http://localhost:9000/login?code=fmzhJ5&state=bn9077
- 我收到此错误:出现意外错误(类型=未授权,状态=401)。
身份验证失败:无法获取访问令牌
更新
我发现以下错误消息:检测到可能的 CSRF - 需要状态参数但找不到状态
我猜 saving/using/converting "state" 参数配置错误。但是我想使用它。
更新2
我找到了 this comment 并为客户端添加了不同的上下文路径。这将错误消息更改为:
org.springframework.security.authentication.BadCredentialsException: 无法获取访问令牌
原因:org.springframework.security.oauth2.client.resource.OAuth2AccessDeniedException:请求访问令牌时出错。
原因:org.springframework.web.client.HttpClientErrorException: 401 null
代码:
客户端应用:
@SpringBootApplication
@EnableOAuth2Sso
public class TestClientApplication
{
public static void main(String[] args) {
SpringApplication.run(TestClientApplication.class, args);
}
}
application.yml:
security:
oauth2:
client:
clientId: client
clientSecret: secret
accessTokenUri: http://localhost:8080/oauth/token
userAuthorizationUri: http://localhost:8080/oauth/authorize
authenticationScheme: query
clientAuthenticationScheme: form
resource:
jwt:
keyValue:
-----BEGIN PUBLIC KEY-----
...
这是我的服务器实现:
OAuth2Config:
@Configuration
@EnableAuthorizationServer
public class OAuth2Config extends AuthorizationServerConfigurerAdapter
{
@Value("${resource.id:spring-boot-application}")
private String resourceId;
@Value("${access_token.validity_period:3600}")
int accessTokenValiditySeconds = 3600;
//todo
private static final String JWTSecretKey = "mySecretKey";
@Autowired
private AuthenticationManager authenticationManager;
@Bean
public TokenEnhancer tokenEnhancer()
{
return new JwtTokenEnhancer();
}
@Bean
protected JwtAccessTokenConverter accessTokenConverter()
{
//todo use secure cert
KeyStoreKeyFactory keyStoreKeyFactory = new KeyStoreKeyFactory(new ClassPathResource("jwt.jks"), JWTSecretKey.toCharArray());
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
converter.setKeyPair(keyStoreKeyFactory.getKeyPair("jwt"));
return converter;
}
@Bean
public TokenStore tokenStore()
{
return new JwtTokenStore(accessTokenConverter());
}
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception
{
TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();
tokenEnhancerChain.setTokenEnhancers(
Arrays.asList(tokenEnhancer(), accessTokenConverter()));
endpoints
.tokenStore(tokenStore())
.tokenEnhancer(tokenEnhancerChain)
.authenticationManager(this.authenticationManager); //only needed for password grant, which we should only use in case of native/client side apps
}
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception
{
//todo add redirect urls
clients.inMemory()
.withClient("trusted-app") //todo for postman testing only, remove in production!
.secret("secret")
.authorizedGrantTypes("client_credentials")
.authorities("ROLE_TRUSTED_CLIENT")
.scopes("read", "write")
.resourceIds(resourceId, "myprintforce")
.autoApprove(true)
.accessTokenValiditySeconds(accessTokenValiditySeconds)
.and()
.withClient("client")
.secret("secret")
.authorizedGrantTypes("password", "authorization_code", "refresh_token", "implicit")
.authorities("ROLE_CLIENT", "ROLE_TRUSTED_CLIENT")
.scopes("read", "write")
.resourceIds("client", resourceId)
.autoApprove(true)
.accessTokenValiditySeconds(accessTokenValiditySeconds);
;
}
}
TokenEnhancer
public class JwtTokenEnhancer implements TokenEnhancer
{
@Override
public OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication)
{
Map<String, Object> additionalInfo = new HashMap<>();
additionalInfo.put("user", authentication.getName());
((DefaultOAuth2AccessToken) accessToken).setAdditionalInformation(additionalInfo);
return accessToken;
}
}
WebSecurityConfig
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter
{
@Override
public void configure(AuthenticationManagerBuilder auth) throws Exception
{
auth.inMemoryAuthentication().withUser("u").password("p").roles("USER");
}
@Override
protected void configure(HttpSecurity http) throws Exception
{
http
.authorizeRequests()
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/login")
.permitAll()
.and();
}
@Override
@Bean
public AuthenticationManager authenticationManagerBean() throws Exception
{
return super.authenticationManagerBean();
}
}
申请
@SpringBootApplication
public class OAuthServerApplication extends WebMvcConfigurerAdapter
{
public static void main(String[] args)
{
SpringApplication.run(OAuthServerApplication.class, args);
}
@Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/login").setViewName("login");
}
}
问题是两台服务器使用同一台 (localhost) 机器。当他们有不同的上下文路径时,问题就差不多解决了。
通过更改配置文件中的一些条目,我让它工作了。
我删除了:authenticationScheme: query
和 clientAuthenticationScheme: form
并添加了 scope=read
现在看起来像这样:
security:
oauth2:
client:
clientId: client
clientSecret: secret
accessTokenUri: http://localhost:12000/authentication/oauth/token
userAuthorizationUri: http://localhost:12000/authentication/oauth/authorize
scope: read
resource:
jwt:
keyValue:
-----BEGIN PUBLIC KEY-----
我正在尝试使用 JWT 和 OAuth2 在 server/client 上获得单一登录。
除了用户名(我打算将其嵌入到 jwt 中)之外,我没有其他资源可以从客户端外部请求。
我的服务器正在运行并且可以很好地处理 jwt 令牌(使用邮递员的 oauth2 令牌身份验证进行测试)。我只是不知道如何处理客户。
我需要让我的客户端成为 ResourceServer 吗?或者我应该直接使用@EnableOAuth2Sso 教程吗?
对于我的客户,我有以下规格:
- 我想使用 "authorization_code" 授权类型
- 我想把客户端的所有页面隐藏在oauth2登录后面
- 用户请求一个页面,如果他们没有登录(没有带有有效 jwt 令牌的身份验证 header),他们应该被重定向到 authserver/oauth/authorize 路径
- 显示登录表单,用户使用用户名密码登录
- 如果登录正确,他们将被重定向回请求的客户端页面
- 如果用户在 JWT 过期之前返回,则他们不必再次登录。
- 我想使用 Spring 安全性,自定义代码越少越好。
我认为这是可能的,并且正是单点登录应该做的。
我的第一个想法是使用 @EnableOAuth2Sso 但这不能正常工作:
- 我被重定向到服务器上的登录页面
- 我正确登录
- 我被重定向到:http://localhost:9000/login?code=fmzhJ5&state=bn9077
- 我收到此错误:出现意外错误(类型=未授权,状态=401)。 身份验证失败:无法获取访问令牌
更新
我发现以下错误消息:检测到可能的 CSRF - 需要状态参数但找不到状态
我猜 saving/using/converting "state" 参数配置错误。但是我想使用它。
更新2
我找到了 this comment 并为客户端添加了不同的上下文路径。这将错误消息更改为:
org.springframework.security.authentication.BadCredentialsException: 无法获取访问令牌 原因:org.springframework.security.oauth2.client.resource.OAuth2AccessDeniedException:请求访问令牌时出错。 原因:org.springframework.web.client.HttpClientErrorException: 401 null
代码:
客户端应用:
@SpringBootApplication
@EnableOAuth2Sso
public class TestClientApplication
{
public static void main(String[] args) {
SpringApplication.run(TestClientApplication.class, args);
}
}
application.yml:
security:
oauth2:
client:
clientId: client
clientSecret: secret
accessTokenUri: http://localhost:8080/oauth/token
userAuthorizationUri: http://localhost:8080/oauth/authorize
authenticationScheme: query
clientAuthenticationScheme: form
resource:
jwt:
keyValue:
-----BEGIN PUBLIC KEY-----
...
这是我的服务器实现:
OAuth2Config:
@Configuration
@EnableAuthorizationServer
public class OAuth2Config extends AuthorizationServerConfigurerAdapter
{
@Value("${resource.id:spring-boot-application}")
private String resourceId;
@Value("${access_token.validity_period:3600}")
int accessTokenValiditySeconds = 3600;
//todo
private static final String JWTSecretKey = "mySecretKey";
@Autowired
private AuthenticationManager authenticationManager;
@Bean
public TokenEnhancer tokenEnhancer()
{
return new JwtTokenEnhancer();
}
@Bean
protected JwtAccessTokenConverter accessTokenConverter()
{
//todo use secure cert
KeyStoreKeyFactory keyStoreKeyFactory = new KeyStoreKeyFactory(new ClassPathResource("jwt.jks"), JWTSecretKey.toCharArray());
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
converter.setKeyPair(keyStoreKeyFactory.getKeyPair("jwt"));
return converter;
}
@Bean
public TokenStore tokenStore()
{
return new JwtTokenStore(accessTokenConverter());
}
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception
{
TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();
tokenEnhancerChain.setTokenEnhancers(
Arrays.asList(tokenEnhancer(), accessTokenConverter()));
endpoints
.tokenStore(tokenStore())
.tokenEnhancer(tokenEnhancerChain)
.authenticationManager(this.authenticationManager); //only needed for password grant, which we should only use in case of native/client side apps
}
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception
{
//todo add redirect urls
clients.inMemory()
.withClient("trusted-app") //todo for postman testing only, remove in production!
.secret("secret")
.authorizedGrantTypes("client_credentials")
.authorities("ROLE_TRUSTED_CLIENT")
.scopes("read", "write")
.resourceIds(resourceId, "myprintforce")
.autoApprove(true)
.accessTokenValiditySeconds(accessTokenValiditySeconds)
.and()
.withClient("client")
.secret("secret")
.authorizedGrantTypes("password", "authorization_code", "refresh_token", "implicit")
.authorities("ROLE_CLIENT", "ROLE_TRUSTED_CLIENT")
.scopes("read", "write")
.resourceIds("client", resourceId)
.autoApprove(true)
.accessTokenValiditySeconds(accessTokenValiditySeconds);
;
}
}
TokenEnhancer
public class JwtTokenEnhancer implements TokenEnhancer
{
@Override
public OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication)
{
Map<String, Object> additionalInfo = new HashMap<>();
additionalInfo.put("user", authentication.getName());
((DefaultOAuth2AccessToken) accessToken).setAdditionalInformation(additionalInfo);
return accessToken;
}
}
WebSecurityConfig
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter
{
@Override
public void configure(AuthenticationManagerBuilder auth) throws Exception
{
auth.inMemoryAuthentication().withUser("u").password("p").roles("USER");
}
@Override
protected void configure(HttpSecurity http) throws Exception
{
http
.authorizeRequests()
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/login")
.permitAll()
.and();
}
@Override
@Bean
public AuthenticationManager authenticationManagerBean() throws Exception
{
return super.authenticationManagerBean();
}
}
申请
@SpringBootApplication
public class OAuthServerApplication extends WebMvcConfigurerAdapter
{
public static void main(String[] args)
{
SpringApplication.run(OAuthServerApplication.class, args);
}
@Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/login").setViewName("login");
}
}
问题是两台服务器使用同一台 (localhost) 机器。当他们有不同的上下文路径时,问题就差不多解决了。
通过更改配置文件中的一些条目,我让它工作了。
我删除了:authenticationScheme: query
和 clientAuthenticationScheme: form
并添加了 scope=read
现在看起来像这样:
security:
oauth2:
client:
clientId: client
clientSecret: secret
accessTokenUri: http://localhost:12000/authentication/oauth/token
userAuthorizationUri: http://localhost:12000/authentication/oauth/authorize
scope: read
resource:
jwt:
keyValue:
-----BEGIN PUBLIC KEY-----