在Spring Security Oauth2-授权码模式(Finchley版本)一文中介绍了OAuth2的授权码模式的实现,本文将在这篇文章的基础上使用JWT生成token。
一、准备工作
- 下载代码
大家可以在github上下载Spring Security Oauth2-授权码模式(Finchley版本)的源码。 - 添加JWT依赖
授权服务和资源服务是两个分开的服务,需要在两个服务中添加JWT依赖<dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-jwt</artifactId> <version>1.0.7.RELEASE</version> </dependency>
二、案例介绍
JWT认证提供了对称加密和非对称加密的实现。
2.1 对称加密
2.1.1 授权服务
(1) 定义token的生成方式
AccessToken转换器用来定义token的生成方式,这里使用JWT生成token
@Bean
public TokenStore tokenStore(){
return new JwtTokenStore(accessTokenConverter());
}
@Bean
public JwtAccessTokenConverter accessTokenConverter() {
final JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
converter.setSigningKey("123");
return converter;
}
(2) 告知spring security token的生成方式
/**
* 用来配置授权(authorization)以及令牌(token)的访问端点和令牌服务(token services)
* @param endpoints
* @throws Exception
*/
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
//指定认证管理器
endpoints.authenticationManager(authenticationManager);
//指定token存储位置
endpoints.tokenStore(tokenStore());
// token生成方式
endpoints.accessTokenConverter(accessTokenConverter());
endpoints.userDetailsService(userDetailsService);
}
2.1.2 资源服务
资源服务的配置与授权服务大致相同
/**
* 资源服务器配置
*
* @author simon
* @create 2018-11-14 11:03
**/
@Configuration
@EnableResourceServer
public class OAuth2ResourceServerConfig extends ResourceServerConfigurerAdapter {
@Bean
public JwtAccessTokenConverter accessTokenConverter(){
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
converter.setSigningKey("123");
return converter;
}
@Bean
public TokenStore tokenStore() {
return new JwtTokenStore(accessTokenConverter());
}
@Override
public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
DefaultTokenServices defaultTokenServices = new DefaultTokenServices();
defaultTokenServices.setTokenStore(tokenStore());
resources.tokenServices(defaultTokenServices;
}
}
2.2 非对称加密
使用非对称密钥(公钥和私钥)来执行签名过程,需要先生成一个证书并导出公钥。
2.2.1 生成证书
(1) 生成JKS Java KeyStore文件
使用命令行工具keytool生成证书
keytool -genkeypair -alias mytest -keyalg RSA -keypass mypass -keystore mytest.jks -storepass mypass
此命令将生成一个名为mytest.jks的文件,其中包含我们的密钥(公钥和私钥)。
(2) 导出公钥
我们可以使用下面的命令从生成的JKS中导出我们的公钥:
keytool -list -rfc --keystore mytest.jks | openssl x509 -inform pem -pubkey
结果如下:
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAgIK2Wt4x2EtDl41C7vfp
OsMquZMyOyteO2RsVeMLF/hXIeYvicKr0SQzVkodHEBCMiGXQDz5prijTq3RHPy2
/5WJBCYq7yHgTLvspMy6sivXN7NdYE7I5pXo/KHk4nz+Fa6P3L8+L90E/3qwf6j3
DKWnAgJFRY8AbSYXt1d5ELiIG1/gEqzC0fZmNhhfrBtxwWXrlpUDT0Kfvf0QVmPR
xxCLXT+tEe1seWGEqeOLL5vXRLqmzZcBe1RZ9kQQm43+a9Qn5icSRnDfTAesQ3Cr
lAWJKl2kcWU1HwJqw+dZRSZ1X4kEXNMyzPdPBbGmU6MHdhpywI7SKZT7mX4BDnUK
eQIDAQAB
-----END PUBLIC KEY-----
-----BEGIN CERTIFICATE-----
MIIDCzCCAfOgAwIBAgIEGtZIUzANBgkqhkiG9w0BAQsFADA2MQswCQYDVQQGEwJ1
czELMAkGA1UECBMCY2ExCzAJBgNVBAcTAmxhMQ0wCwYDVQQDEwR0ZXN0MB4XDTE2
MDMxNTA4MTAzMFoXDTE2MDYxMzA4MTAzMFowNjELMAkGA1UEBhMCdXMxCzAJBgNV
BAgTAmNhMQswCQYDVQQHEwJsYTENMAsGA1UEAxMEdGVzdDCCASIwDQYJKoZIhvcN
AQEBBQADggEPADCCAQoCggEBAICCtlreMdhLQ5eNQu736TrDKrmTMjsrXjtkbFXj
Cxf4VyHmL4nCq9EkM1ZKHRxAQjIhl0A8+aa4o06t0Rz8tv+ViQQmKu8h4Ey77KTM
urIr1zezXWBOyOaV6Pyh5OJ8/hWuj9y/Pi/dBP96sH+o9wylpwICRUWPAG0mF7dX
eRC4iBtf4BKswtH2ZjYYX6wbccFl65aVA09Cn739EFZj0ccQi10/rRHtbHlhhKnj
iy+b10S6ps2XAXtUWfZEEJuN/mvUJ+YnEkZw30wHrENwq5QFiSpdpHFlNR8CasPn
WUUmdV+JBFzTMsz3TwWxplOjB3YacsCO0imU+5l+AQ51CnkCAwEAAaMhMB8wHQYD
VR0OBBYEFOGefUBGquEX9Ujak34PyRskHk+WMA0GCSqGSIb3DQEBCwUAA4IBAQB3
1eLfNeq45yO1cXNl0C1IQLknP2WXg89AHEbKkUOA1ZKTOizNYJIHW5MYJU/zScu0
yBobhTDe5hDTsATMa9sN5CPOaLJwzpWV/ZC6WyhAWTfljzZC6d2rL3QYrSIRxmsp
/J1Vq9WkesQdShnEGy7GgRgJn4A8CKecHSzqyzXulQ7Zah6GoEUD+vjb+BheP4aN
hiYY1OuXD+HsdKeQqS+7eM5U7WW6dz2Q8mtFJ5qAxjY75T0pPrHwZMlJUhUZ+Q2V
FfweJEaoNB9w9McPe1cAiE+oeejZ0jq0el3/dJsx3rlVqZN+lMhRJJeVHFyeb3XF
lLFCUGhA7hxn2xf3x1JW
-----END CERTIFICATE-----
这里我们只需要复制公钥到资源服务的resources目录下的public.txt 文件中
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAgIK2Wt4x2EtDl41C7vfp
OsMquZMyOyteO2RsVeMLF/hXIeYvicKr0SQzVkodHEBCMiGXQDz5prijTq3RHPy2
/5WJBCYq7yHgTLvspMy6sivXN7NdYE7I5pXo/KHk4nz+Fa6P3L8+L90E/3qwf6j3
DKWnAgJFRY8AbSYXt1d5ELiIG1/gEqzC0fZmNhhfrBtxwWXrlpUDT0Kfvf0QVmPR
xxCLXT+tEe1seWGEqeOLL5vXRLqmzZcBe1RZ9kQQm43+a9Qn5icSRnDfTAesQ3Cr
lAWJKl2kcWU1HwJqw+dZRSZ1X4kEXNMyzPdPBbGmU6MHdhpywI7SKZT7mX4BDnUK
eQIDAQAB
-----END PUBLIC KEY-----
2.2.2 授权服务
将刚刚生成的证书复制到授权服务器的resources目录下。配置JwtAccessTokenConverter使用mytest.jks 中的KeyPair
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
KeyStoreKeyFactory keyStoreKeyFactory =
new KeyStoreKeyFactory(new ClassPathResource("mytest.jks"), "mypass".toCharArray());
converter.setKeyPair(keyStoreKeyFactory.getKeyPair("mytest"));
return converter;
}
2.2.3 资源服务
配置资源服务器使用公钥:
@Bean
public JwtAccessTokenConverter accessTokenConverter(){
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
Resource resource = new ClassPathResource("public.txt");
String publicKey;
try {
publicKey = inputStream2String(resource.getInputStream());
} catch (final IOException e) {
throw new RuntimeException(e);
}
converter.setVerifierKey(publicKey);
return converter;
}
String inputStream2String(InputStream is) throws IOException {
BufferedReader in = new BufferedReader(new InputStreamReader(is));
StringBuffer buffer = new StringBuffer();
String line = "";
while ((line = in.readLine()) != null) {
buffer.append(line);
}
return buffer.toString();
}
2.3 添加额外信息
额外信息的添加与加密方式无关
2.3.1 自定义生成token携带的信息
可以自定义一个TokenEnhancer将额外的信息添加到token中。TokenEnhancer 接口提供public OAuth2AccessToken enhance(OAuth2AccessToken oAuth2AccessToken, OAuth2Authentication oAuth2Authentication)
方法用于token信息的添加
(1) 自定义TokenEnhancer
/**
* 自定义token生成携带的信息
*
* @author simon
* @create 2018-11-14 10:16
**/
public class CustomTokenEnhancer implements TokenEnhancer {
@Override
public OAuth2AccessToken enhance(OAuth2AccessToken oAuth2AccessToken, OAuth2Authentication oAuth2Authentication) {
final Map<String, Object> additionalInfo = new HashMap<>();
//获取登录信息
UserDetails user = (UserDetails) oAuth2Authentication.getUserAuthentication().getPrincipal();
additionalInfo.put("userName", user.getUsername());
additionalInfo.put("authorities", user.getAuthorities());
((DefaultOAuth2AccessToken) oAuth2AccessToken).setAdditionalInformation(additionalInfo);
return oAuth2AccessToken;
}
}
(2) 将自定义的TokenEnhancer加入到TokenEnhancerChain中
/**
* 用来配置授权(authorization)以及令牌(token)的访问端点和令牌服务(token services)
* @param endpoints
* @throws Exception
*/
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
//指定认证管理器
endpoints.authenticationManager(authenticationManager);
//指定token存储位置
endpoints.tokenStore(tokenStore());
endpoints.accessTokenConverter(accessTokenConverter());
endpoints.userDetailsService(userDetailsService);
//自定义token生成方式
TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();
tokenEnhancerChain.setTokenEnhancers(Arrays.asList(customerEnhancer(),accessTokenConverter()));
endpoints.tokenEnhancer(tokenEnhancerChain);
2.3.2 自定义token中添加的信息
(1)授权服务自定义JwtAccessTokenConverte
JwtAccessTokenConverter是我们用来生成token的转换器,所以我们需要配置这里面的部分信息来实现token中携带额外的信息。
JwtAccessTokenConverter默认使用DefaultAccessTokenConverter来处理token的生成、转换、获取。DefaultAccessTokenConverter中使用UserAuthenticationConverter来处理token与userinfo的获取、转换。因此我们需要重写下UserAuthenticationConverter对应的转换方法就可以
/**
* 自定义CustomerAccessTokenConverter 这个类的作用主要用于AccessToken的转换,
* 默认使用DefaultAccessTokenConverter 这个装换器
* DefaultAccessTokenConverter有个UserAuthenticationConverter,这个转换器作用是把用户的信息放入token中,默认只是放入user_name
* <p>
* 自定义这个方法,加入了额外的信息
* <p>
* @author simon
* @create 2018-11-14 10:26
**/
public class CustomerAccessTokenConverter extends DefaultAccessTokenConverter {
public CustomerAccessTokenConverter() {
super.setUserTokenConverter(new CustomerUserAuthenticationConverter());
}
private class CustomerUserAuthenticationConverter extends DefaultUserAuthenticationConverter{
@Override
public Map<String, ?> convertUserAuthentication(Authentication authentication) {
LinkedHashMap <String, Object> response = new LinkedHashMap <>();
response.put("details", authentication.getDetails());
response.put("test","hello");
if (authentication.getAuthorities() != null && !authentication.getAuthorities().isEmpty()) {
response.put("authorities", AuthorityUtils.authorityListToSet(authentication.getAuthorities()));
}
return response;
}
}
}
(2) 授权服务告诉JwtAccessTokenConverter替换默认的方式
@Bean
public JwtAccessTokenConverter accessTokenConverter() {
final JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
KeyStoreKeyFactory keyStoreKeyFactory =
new KeyStoreKeyFactory(new ClassPathResource("mytest.jks"), "mypass".toCharArray());
converter.setKeyPair(keyStoreKeyFactory.getKeyPair("mytest"));
converter.setAccessTokenConverter(new CustomerAccessTokenConverter());
return converter;
}
(3)资源服务自定义JwtAccessTokenConverte
/**
* 自定义CustomerAccessTokenConverter 这个类的作用主要用于AccessToken的转换,
* 默认使用DefaultAccessTokenConverter 这个装换器
* DefaultAccessTokenConverter有个UserAuthenticationConverter,这个转换器作用是把用户的信息放入token中,
* 默认只是放入username
* <p>
* 自定义了下这个方法,加入了额外的信息
* <p>
* @author simon
* @create 2018-11-14 10:26
**/
public class CustomerAccessTokenConverter extends DefaultAccessTokenConverter {
public CustomerAccessTokenConverter() {
super.setUserTokenConverter(new CustomerUserAuthenticationConverter());
}
private class CustomerUserAuthenticationConverter extends DefaultUserAuthenticationConverter {
// 资源服务获得自定义信息
@Override
public Authentication extractAuthentication(Map<String, ?> map) {
Collection <? extends GrantedAuthority> authorities = this.getAuthorities(map);
return new UsernamePasswordAuthenticationToken(map, "N/A", authorities);
}
private Collection<? extends GrantedAuthority> getAuthorities(Map<String, ?> map) {
if (!map.containsKey("authorities")) {
return AuthorityUtils.commaSeparatedStringToAuthorityList(StringUtils.arrayToCommaDelimitedString(new String[]{"USER"}));
} else {
Object authorities = map.get("authorities");
if (authorities instanceof String) {
return AuthorityUtils.commaSeparatedStringToAuthorityList((String) authorities);
} else if (authorities instanceof Collection) {
return AuthorityUtils.commaSeparatedStringToAuthorityList(StringUtils.collectionToCommaDelimitedString((Collection) authorities));
} else {
throw new IllegalArgumentException("Authorities must be either a String or a Collection");
}
}
}
}
}
2.4 测试
启动服务
2.4.1 获取code
浏览器访问http://localhost:8080/oauth/authorize?response_type=code&client_id=client1&redirect_uri=http://baidu.com
进入登录页面,输入用户名:admin;密码:admin。
登录成功进入授权页面,点击授权,获得code
https://www.baidu.com/?code=NW8eB1
2.4.2 获取token
使用POSTMAN发送post请求获取token
postman请求
2.4.3 访问资源服务获取资源
使用POSTMAN发送get请求获取资源
postman请求
2.4.4 解析token
新增测试类解析token
@Test
public void contextLoads() {
//填写token
String token = "";
Jwt jwt = JwtHelper.decode(token);
System.err.println(jwt.toString());
}
解析后的信息如下:
{"alg":"RS256","typ":"JWT"} {"test":"hello","scope":["test"],"details":{"remoteAddress":"0:0:0:0:0:0:0:1","sessionId":"34B189EA6F1DA4834E5AEA31E91A2460"},"exp":1542277115,"userName":"admin","authorities":[{"authority":"USER"}],"jti":"8e4a72d3-affb-4977-b174-cb9ee4f2e08b","client_id":"client1"} [256 crypto bytes]
结果中包含添加的额外信息
网友评论