Spring Security OAuth2實現使用JWT
在Spring Security Oauth2-授權碼模式(Finchley版本)一文中介紹了OAuth2的授權碼模式的實現,本文將在這篇文章的基礎上使用JWT生成token。關於JWT的介紹可以參考JWT詳解。
一、準備工作
- 新增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。
2.4.2 獲取token
使用POSTMAN傳送post請求獲取token
2.4.3 訪問資源服務獲取資源
使用POSTMAN傳送get請求獲取資源
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]
結果中包含新增的額外資訊