1. 程式人生 > >Spring Security原始碼分析十一:Spring Security OAuth2整合JWT

Spring Security原始碼分析十一:Spring Security OAuth2整合JWT

Json web token (JWT), 是為了在網路應用環境間傳遞宣告而執行的一種基於JSON的開放標準(RFC 7519).該token被設計為緊湊且安全的,特別適用於分散式站點的單點登入(SSO)場景。JWT的宣告一般被用來在身份提供者和服務提供者間傳遞被認證的使用者身份資訊,以便於從資源伺服器獲取資源,也可以增加一些額外的其它業務邏輯所必須的宣告資訊,該token也可直接被用於認證,也可被加密。

JWT組成

https://raw.githubusercontent.com/longfeizheng/longfeizheng.github.io/master/images/security/spring-security-jwt01.png

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX25hbWUiOiJhZG1pbiIsInNjb3BlIjpbImFsbCJdLCJleHAiOjE1MTY3MjY4MTMsImJsb2ciOiJodHRwczovL2xvbmdmZWl6aGVuZy5naXRodWIuaW8vIiwiYXV0aG9yaXRpZXMiOlsiUk9MRV9VU0VSIl0sImp0aSI6ImJmZmY0NjRjLTFiNTktNGZkNy1hNTE4LWU3YjY5MDFiNzU3YyIsImNsaWVudF9pZCI6Im1lcnJ5eW91In0.gp
5t9nY9mGp5O2-yqdflc0nEAsTeCQG7VugA8q8XcF4

Header 包含了一些元資料,至少會表明 token 型別以及 簽名方法。

{
 "typ": "JWT",
 "alg": "HS256"
}

Claims (Payload)

Claims 部分包含了一些跟這個 token 有關的重要資訊。

{
 "user_name": "admin",
 "scope": [
  "all"
 ],
 "exp": 1516726813,
 "blog": "https://longfeizheng.github.io/",
 "authorities
": [ "ROLE_USER" ], "jti": "bfff464c-1b59-4fd7-a518-e7b6901b757c", "client_id": "merryyou" }

Signature

JWT 標準遵照 JSON Web Signature (JWS) 標準來生成簽名。簽名主要用於驗證 token 是否有效,是否被篡改。

JWT流程示意圖

https://raw.githubusercontent.com/longfeizheng/longfeizheng.github.io/master/images/security/spring-security-jwt02.jpg

Spring Security Oauth2 實現JWT

配置TokenStoreConfig用於儲存Token

@Configuration
public class TokenStoreConfig
{
/** * redis連線工廠 */ @Autowired private RedisConnectionFactory redisConnectionFactory; /** * 用於存放token * @return */ @Bean @ConditionalOnProperty(prefix = "merryyou.security.oauth2", name = "storeType", havingValue = "redis") public TokenStore redisTokenStore() { return new RedisTokenStore(redisConnectionFactory); } /** * jwt TOKEN配置資訊 */ @Configuration @ConditionalOnProperty(prefix = "merryyou.security.oauth2", name = "storeType", havingValue = "jwt", matchIfMissing = true) public static class JwtTokenCofnig{ /** * 使用jwtTokenStore儲存token * @return */ @Bean public TokenStore jwtTokenStore(){ return new JwtTokenStore(jwtAccessTokenConverter()); } /** * 用於生成jwt * @return */ @Bean public JwtAccessTokenConverter jwtAccessTokenConverter(){ JwtAccessTokenConverter accessTokenConverter = new JwtAccessTokenConverter(); accessTokenConverter.setSigningKey("merryyou");//生成簽名的key return accessTokenConverter; } /** * 用於擴充套件JWT * @return */ @Bean @ConditionalOnMissingBean(name = "jwtTokenEnhancer") public TokenEnhancer jwtTokenEnhancer(){ return new MerryyouJwtTokenEnhancer(); } } }
MerryyouJwtTokenEnhancer
public class MerryyouJwtTokenEnhancer implements TokenEnhancer {
    @Override
    public OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication) {
        Map<String, Object> info = new HashMap<>();
        info.put("blog", "https://longfeizheng.github.io/");//擴充套件返回的token
        ((DefaultOAuth2AccessToken) accessToken).setAdditionalInformation(info);
        return accessToken;
    }
}

配置認證伺服器

@Configuration
@EnableAuthorizationServer
public class MerryyouAuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {

    @Autowired
    private AuthenticationManager authenticationManager;

    @Autowired
    private UserDetailsService userDetailsService;

    @Autowired
    private TokenStore tokenStore;

    @Autowired(required = false)
    private JwtAccessTokenConverter jwtAccessTokenConverter;

    @Autowired(required = false)
    private TokenEnhancer jwtTokenEnhancer;

    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        endpoints.tokenStore(tokenStore)
                .authenticationManager(authenticationManager)
                .userDetailsService(userDetailsService);
        //擴充套件token返回結果
        if (jwtAccessTokenConverter != null && jwtTokenEnhancer != null) {
            TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();
            List<TokenEnhancer> enhancerList = new ArrayList();
            enhancerList.add(jwtTokenEnhancer);
            enhancerList.add(jwtAccessTokenConverter);
            tokenEnhancerChain.setTokenEnhancers(enhancerList);
            //jwt
            endpoints.tokenEnhancer(tokenEnhancerChain)
                    .accessTokenConverter(jwtAccessTokenConverter);
        }
    }

    /**
     * 配置客戶端一些資訊
     * @param clients
     * @throws Exception
     */
    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        clients.inMemory()
                .withClient("merryyou")
                .secret("merryyou")
                .accessTokenValiditySeconds(7200)
                .authorizedGrantTypes("refresh_token", "password", "authorization_code")//OAuth2支援的驗證模式
                .scopes("all");
    }
}

配置資源伺服器

@Configuration
@EnableResourceServer
public class MerryyouResourceServerConfig extends ResourceServerConfigurerAdapter {

    /**
     * 自定義登入成功處理器
     */
    @Autowired
    private AuthenticationSuccessHandler appLoginInSuccessHandler;

    @Override
    public void configure(HttpSecurity http) throws Exception {
        http.formLogin()
                .successHandler(appLoginInSuccessHandler)//登入成功處理器
                .and()
                .authorizeRequests().anyRequest().authenticated().and()
                .csrf().disable();
    }

}

解析擴充套件的Token

 @GetMapping("/user")
    public Object getCurrentUser1(Authentication authentication, HttpServletRequest request) throws UnsupportedEncodingException {
        log.info("【SecurityOauth2Application】 getCurrentUser1 authenticaiton={}", JsonUtil.toJson(authentication));

        String header = request.getHeader("Authorization");
        String token = StringUtils.substringAfter(header, "bearer ");

        Claims claims = Jwts.parser().setSigningKey("merryyou".getBytes("UTF-8")).parseClaimsJws(token).getBody();
        String blog = (String) claims.get("blog");
        log.info("【SecurityOauth2Application】 getCurrentUser1 blog={}", blog);

        return authentication;
    }

測試方法

    @Test
    public void signInTest() throws Exception {
        RestTemplate rest = new RestTemplate();
        HttpHeaders headers = new HttpHeaders();
        headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
        headers.add("authorization", getBasicAuthHeader());

        MultiValueMap<String, String> params = new LinkedMultiValueMap<>();
        params.add("username", "admin");
        params.add("password", "123456");

        HttpEntity<?> entity = new HttpEntity(params, headers);
        // pay attention, if using get with headers, should use exchange instead of getForEntity / getForObject
        ResponseEntity<String> result = rest.exchange(SIGN_IN_URI, HttpMethod.POST, entity, String.class, new Object[]{null});
        log.info("登入資訊返回的結果={}", JsonUtil.toJson(result));
    }

列印:

 "body": "{\"access_token\":\"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX25hbWUiOiJhZG1pbiIsInNjb3BlIjpbImFsbCJdLCJleHAiOjE1MTY3MjkxNDIsImJsb2ciOiJodHRwczovL2xvbmdmZWl6aGVuZy5naXRodWIuaW8vIiwiYXV0aG9yaXRpZXMiOlsiUk9MRV9VU0VSIl0sImp0aSI6IjMzOTUxNDk1LTBjOGYtNGQ5NS1iZDYyLTAxMjEyYWNjZDU1ZCIsImNsaWVudF9pZCI6Im1lcnJ5eW91In0.7Lrpmn3CaNweqcMeADJeZJGDTEZYN-gg5OpAzbKIEqQ\",\"token_type\":\"bearer\",\"refresh_token\":\"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX25hbWUiOiJhZG1pbiIsInNjb3BlIjpbImFsbCJdLCJhdGkiOiIzMzk1MTQ5NS0wYzhmLTRkOTUtYmQ2Mi0wMTIxMmFjY2Q1NWQiLCJleHAiOjE1MTkzMTM5NDIsImJsb2ciOiJodHRwczovL2xvbmdmZWl6aGVuZy5naXRodWIuaW8vIiwiYXV0aG9yaXRpZXMiOlsiUk9MRV9VU0VSIl0sImp0aSI6IjFlMjI1YzE5LTE5NDMtNGNjMi1iYTdjLTM1MzdmZDA1M2E4MyIsImNsaWVudF9pZCI6Im1lcnJ5eW91In0.lKHgXd2HSPCp2cK6S-ZvLUwXRjnXEX9wryDWV4CmSGw\",\"expires_in\":7199,\"scope\":\"all\",\"blog\":\"https://longfeizheng.github.io/\",\"jti\":\"33951495-0c8f-4d95-bd62-01212accd55d\"}"

效果如下:

https://raw.githubusercontent.com/longfeizheng/longfeizheng.github.io/master/images/security/spring-security-jwt03.gif

程式碼下載