需求背景
本篇文章讲解如何通过Springboot2集成验证服务JWT Token,以及资源服务的用法(更多官方关于 Oauth2)
此篇在《Oauth2+Token详细用法/SpringSecurity》基础上修改而来,可先去阅读此篇文章
以下内容:主要是讲解用法差异的地方
OAuth2术语
- JWT
- JSON Web Token 身份令牌
Oauth2 密码授权流程
在oauth2协议中,一个应用会有自己的clientId和clientSecret(从认证方申请),由认证方下发clientId和secret
代码演示
授权服务 Authorization Server
构建Authorization Server,这里我使用了SpringSecurity5 和 SpringBoot 2.0.6版本
1. 项目目录结构
Spring Security Configuration 部分
2. SecurityProperties.java文件
先使用注解@ConfigurationProperties,绑定配置文件(后面会看到)
package com.md.demo.oauth.jwt.config.props;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.core.io.Resource;
@ConfigurationProperties("security")
public class SecurityProperties {
private JwtProperties jwt;
public JwtProperties getJwt() {
return jwt;
}
public void setJwt(JwtProperties jwt) {
this.jwt = jwt;
}
public static class JwtProperties {
private Resource keyStore;
private String keyStorePassword;
private String keyPairAlias;
private String keyPairPassword;
public Resource getKeyStore() {
return keyStore;
}
public void setKeyStore(Resource keyStore) {
this.keyStore = keyStore;
}
public String getKeyStorePassword() {
return keyStorePassword;
}
public void setKeyStorePassword(String keyStorePassword) {
this.keyStorePassword = keyStorePassword;
}
public String getKeyPairAlias() {
return keyPairAlias;
}
public void setKeyPairAlias(String keyPairAlias) {
this.keyPairAlias = keyPairAlias;
}
public String getKeyPairPassword() {
return keyPairPassword;
}
public void setKeyPairPassword(String keyPairPassword) {
this.keyPairPassword = keyPairPassword;
}
}
}
3. AuthorizationServerConfiguration.java文件
此类中,你会看到所有JWT需要Spring的@Bean注解,其中最重要的是:JwtAccessTokenConverter, JwtTokenStore and the DefaultTokenServices
package com.md.demo.oauth.jwt.config.security;
import java.security.KeyPair;
import javax.sql.DataSource;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.PasswordEncoder;
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;
import org.springframework.security.oauth2.provider.ClientDetailsService;
import org.springframework.security.oauth2.provider.token.DefaultTokenServices;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;
import org.springframework.security.oauth2.provider.token.store.JwtTokenStore;
import org.springframework.security.oauth2.provider.token.store.KeyStoreKeyFactory;
import com.md.demo.oauth.jwt.config.props.SecurityProperties;
@Configuration
@EnableAuthorizationServer
@EnableConfigurationProperties(SecurityProperties.class)
public class AuthorizationServerConfiguration extends AuthorizationServerConfigurerAdapter {
private final DataSource dataSource;
private final PasswordEncoder passwordEncoder;
private final AuthenticationManager authenticationManager;
private final SecurityProperties securityProperties;
private final UserDetailsService userDetailsService;
private JwtAccessTokenConverter jwtAccessTokenConverter;
private TokenStore tokenStore;
public AuthorizationServerConfiguration(final DataSource dataSource, final PasswordEncoder passwordEncoder,
final AuthenticationManager authenticationManager, final SecurityProperties securityProperties,
final UserDetailsService userDetailsService) {
this.dataSource = dataSource;
this.passwordEncoder = passwordEncoder;
this.authenticationManager = authenticationManager;
this.securityProperties = securityProperties;
this.userDetailsService = userDetailsService;
}
@Bean
public TokenStore tokenStore() {
if (tokenStore == null) {
tokenStore = new JwtTokenStore(jwtAccessTokenConverter());
}
return tokenStore;
}
@Bean
public DefaultTokenServices tokenServices(final TokenStore tokenStore,
final ClientDetailsService clientDetailsService) {
DefaultTokenServices tokenServices = new DefaultTokenServices();
tokenServices.setSupportRefreshToken(true);
tokenServices.setTokenStore(tokenStore);
tokenServices.setClientDetailsService(clientDetailsService);
tokenServices.setAuthenticationManager(this.authenticationManager);
return tokenServices;
}
@Bean
public JwtAccessTokenConverter jwtAccessTokenConverter() {
if (jwtAccessTokenConverter != null) {
return jwtAccessTokenConverter;
}
SecurityProperties.JwtProperties jwtProperties = securityProperties.getJwt();
KeyPair keyPair = keyPair(jwtProperties, keyStoreKeyFactory(jwtProperties));
jwtAccessTokenConverter = new JwtAccessTokenConverter();
jwtAccessTokenConverter.setKeyPair(keyPair);
return jwtAccessTokenConverter;
}
@Override
public void configure(final ClientDetailsServiceConfigurer clients) throws Exception {
clients.jdbc(this.dataSource);
}
@Override
public void configure(final AuthorizationServerEndpointsConfigurer endpoints) {
endpoints.authenticationManager(this.authenticationManager)
.accessTokenConverter(jwtAccessTokenConverter())
.userDetailsService(this.userDetailsService)
.tokenStore(tokenStore());
}
@Override
public void configure(final AuthorizationServerSecurityConfigurer oauthServer) {
oauthServer.passwordEncoder(this.passwordEncoder).tokenKeyAccess("permitAll()")
.checkTokenAccess("isAuthenticated()");
}
private KeyPair keyPair(SecurityProperties.JwtProperties jwtProperties, KeyStoreKeyFactory keyStoreKeyFactory) {
return keyStoreKeyFactory.getKeyPair(jwtProperties.getKeyPairAlias(), jwtProperties.getKeyPairPassword().toCharArray());
}
private KeyStoreKeyFactory keyStoreKeyFactory(SecurityProperties.JwtProperties jwtProperties) {
return new KeyStoreKeyFactory(jwtProperties.getKeyStore(), jwtProperties.getKeyStorePassword().toCharArray());
}
}
解释说明
JwtAccessTokenConverter:
用自己的签名证书,去生成tokenJwtTokenStore:从token当中读取数据,它不是真正的一个存储,因为它不持久化任何东西,它实际上是使用JwtAccessTokenConverter去生成和读取token.
DefaultTokenServices:使用
TokenStore
去存储tokens注:这里可以去生成一个自己的签名证书
4. application.yml文件
当你生成自己的签名证书后,配置如下
server:
port: 9000
security:
jwt:
key-store: classpath:keystore.jks
key-store-password: letmein
key-pair-alias: mytestkey
key-pair-password: changeme
spring:
jackson:
serialization:
INDENT_OUTPUT: true
资源服务 Resource Server
1. 项目目录结构
2. SecurityProperties.java文件
为了解码JWT token,我们将要使用在生成自己的签名证书时的public key
在此之前,我们需要先通过@ConfigurationProperties注解绑定到配置文件
package com.md.demo.oauth.jwt.ds.config;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.core.io.Resource;
@ConfigurationProperties("security")
public class SecurityProperties {
private JwtProperties jwt;
public JwtProperties getJwt() {
return jwt;
}
public void setJwt(JwtProperties jwt) {
this.jwt = jwt;
}
public static class JwtProperties {
private Resource publicKey;
public Resource getPublicKey() {
return publicKey;
}
public void setPublicKey(Resource publicKey) {
this.publicKey = publicKey;
}
}
}
使用下面的命令,导出public key
$ keytool -list -rfc --keystore keystore.jks | openssl x509 -inform pem -pubkey -noout
案例结果如下:
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAmWI2jtKwvf0W1hdMdajc
h+mFx9FZe3CZnKNvT/d0+2O6V1Pgkz7L2FcQx2uoV7gHgk5mmb2MZUsy/rDKj0dM
fLzyXqBcCRxD6avALwu8AAiGRxe2dl8HqIHyo7P4R1nUaea1WCZB/i7AxZNAQtcC
cSvMvF2t33p3vYXY6SqMucMD4yHOTXexoWhzwRqjyyC8I8uCYJ+xIfQvaK9Q1RzK
Rj99IRa1qyNgdeHjkwW9v2Fd4O/Ln1Tzfnk/dMLqxaNsXPw37nw+OUhycFDPPQF/
H4Q4+UDJ3ATf5Z2yQKkUQlD45OO2mIXjkWprAmOCi76dLB2yzhCX/plGJwcgb8XH
EQIDAQAB
-----END PUBLIC KEY-----
把内容复制到public.txt文件中,然后配置到application.yml文件中
3. application.yml配置
server:
port: 9100
security:
jwt:
public-key: classpath:public.txt
spring:
jackson:
serialization:
INDENT_OUTPUT: true
4. ResourceServerConfiguration.java文件
用法同AuthorizationServerConfiguration.java文件解释,不重复解释了,可见上面解释
package com.md.demo.oauth.jwt.ds.config;
import static java.nio.charset.StandardCharsets.UTF_8;
import java.io.IOException;
import org.apache.commons.io.IOUtils;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;
import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configurers.ResourceServerSecurityConfigurer;
import org.springframework.security.oauth2.provider.token.DefaultTokenServices;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;
import org.springframework.security.oauth2.provider.token.store.JwtTokenStore;
@Configuration
@EnableResourceServer
@EnableConfigurationProperties(SecurityProperties.class)
public class ResourceServerConfiguration extends ResourceServerConfigurerAdapter {
private static final String ROOT_PATTERN = "/**";
private final SecurityProperties securityProperties;
private TokenStore tokenStore;
public ResourceServerConfiguration(final SecurityProperties securityProperties) {
this.securityProperties = securityProperties;
}
@Override
public void configure(final ResourceServerSecurityConfigurer resources) {
resources.tokenStore(tokenStore());
}
@Override
public void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
// 排除路径验证
.antMatchers("/hello")
.permitAll()
.antMatchers(HttpMethod.GET, ROOT_PATTERN).access("#oauth2.hasScope('read')")
.antMatchers(HttpMethod.POST, ROOT_PATTERN).access("#oauth2.hasScope('write')")
.antMatchers(HttpMethod.PATCH, ROOT_PATTERN).access("#oauth2.hasScope('write')")
.antMatchers(HttpMethod.PUT, ROOT_PATTERN).access("#oauth2.hasScope('write')")
.antMatchers(HttpMethod.DELETE, ROOT_PATTERN).access("#oauth2.hasScope('write')");
}
@Bean
public DefaultTokenServices tokenServices(final TokenStore tokenStore) {
DefaultTokenServices tokenServices = new DefaultTokenServices();
tokenServices.setTokenStore(tokenStore);
return tokenServices;
}
@Bean
public TokenStore tokenStore() {
if (tokenStore == null) {
tokenStore = new JwtTokenStore(jwtAccessTokenConverter());
}
return tokenStore;
}
@Bean
public JwtAccessTokenConverter jwtAccessTokenConverter() {
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
converter.setVerifierKey(getPublicKeyAsString());
return converter;
}
private String getPublicKeyAsString() {
try {
return IOUtils.toString(securityProperties.getJwt().getPublicKey().getInputStream(), UTF_8);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
测试访问
1. 访问受保护接口:http://localhost:9100/me,默认失败
2. 获得token接口:http://localhost:9000/oauth/token?grant_type=password&username=user&password=pass
3. 再访问受保护接口:http://localhost:9100/me?access_token=eyJhbGciOiJSUzI1NiIxxxx....xxxx,完整的access_token值(这里省略复制了),即可访问成功
完整源码下载
- OAuth2 JWT Server端对应的Github源码地址
- OAuth2 JWT Resource端对应的Github源码地址
该系列教程
至此,全部介绍就结束了
------------------------------------------------------
------------------------------------------------------
关于我(个人域名)
期望和大家一起学习,一起成长,共勉,O(∩_∩)O谢谢
欢迎交流问题,可加个人QQ 469580884,
或者,加我的群号 751925591,一起探讨交流问题
不讲虚的,只做实干家
Talk is cheap,show me the code