【Oauth2】實現表單登錄後返回jwt token

背景

由於前後端分離的原因,在使用默認的表單登錄時,希望能像密碼模式一樣直接返回JWT信息。(爲什麼不用授權碼模式?用,但想保留默認的表單登錄)

 思想

通過認證成功後的成功處理器AuthenticationSuccessHandler,來處理登錄後進行jwt生成並返回的流程。

走過的彎路

使用OAuth2RestTemplate

用OAuth2RestTemplate來進行API訪問,其實就是多進行一次遠程請求,最大的問題是客戶端密碼後臺是不知道的,而且本就是認證中心內部的處理,爲何需要遠程調用,這種方案其實行不通。

ResourceOwnerPasswordResourceDetails resource = new ResourceOwnerPasswordResourceDetails();

List<String> scopes = new ArrayList<String>();
scopes.add("read");
resource.setAccessTokenUri("http://localhost:8000/oauth/token");
resource.setClientId("myclient");
resource.setClientSecret("mysecret");
resource.setGrantType("password");
resource.setScope(scopes);

resource.setUsername("user");
resource.setPassword("1962FBA51750B9DFDACCCE51");

AccessTokenRequest atr = new DefaultAccessTokenRequest();

OAuth2RestTemplate oTemplate = new OAuth2RestTemplate(resource, new DefaultOAuth2ClientContext(atr));
OAuth2AccessToken token = oTemplate.getAccessToken();

使用AuthorizationServerTokenServices或者DefaultTokenServices

這種其實是參考https://www.jianshu.com/p/19059060036b,得出來的代碼,實際執行發現,這種適合默認token方式,而不適合JWT形式,而且在本地環境還會報tokenStore的NPE異常,在此行不通。

@Component
public class MyAuthenticationSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {
    @Autowired
    private ClientDetailsService clientDetailsService;

    @Autowired
    private DefaultTokenServices authorizationServerTokenServices;


    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        //獲取clientId
        String clientId = "myclient";

        //獲取 ClientDetails
        ClientDetails clientDetails = clientDetailsService.loadClientByClientId(clientId);

        if (clientDetails == null){
            throw new UnapprovedClientAuthenticationException("clientId 不存在"+clientId);
        }
        //密碼授權 模式, 組建 authentication
        TokenRequest tokenRequest = new TokenRequest(MapUtils.EMPTY_MAP,clientId,clientDetails.getScope(),"password");

        OAuth2Request oAuth2Request = tokenRequest.createOAuth2Request(clientDetails);
        OAuth2Authentication oAuth2Authentication = new OAuth2Authentication(oAuth2Request,authentication);

        OAuth2AccessToken token = authorizationServerTokenServices.createAccessToken(oAuth2Authentication);

        //判斷是json 格式返回 還是 view 格式返回
        //將 authention 信息打包成json格式返回
        response.setContentType("application/json;charset=UTF-8");
        response.getWriter().write(JSON.toJSONString(token));
    }

正確步驟

研究源碼TokenEndpoint

由於TokenStore處有NPE問題,對源碼研究後發現,TokenEndpoint是最終的JWT授權生成業務,所以着力去研究發現

 其實JWT是令牌授權者TokenGranter通過TokenRequest參數獲取來的,通過對此處打斷點發現,授權者不就是認證中心配置下的AuthorizationServerEndpointsConfigurer嗎?

OAuth2AccessToken token = getTokenGranter().grant(tokenRequest.getGrantType(), tokenRequest);

 改造認證中心配置

爲認證中心配置增加私有變量myEndpoint,用來存儲AuthorizationServerEndpointsConfigurer實例,這個實例就會有我們定義好的TokenStore(本例爲JwtTokenStore)。然後通過getter方法暴露出來供使用。

@Configuration
@EnableAuthorizationServer
public class AuthenticationServerConfig extends AuthorizationServerConfigurerAdapter {

    @Autowired
    @Qualifier("authenticationManagerBean")
    private AuthenticationManager authenticationManager;

    @Qualifier("dataSource")
    @Autowired
    DataSource dataSource;

    @Autowired
    @Qualifier("userDetailsService")
    UserDetailsService userDetailsService;
    
    @Autowired
    TokenEnhancer myTokenEnhancer;
    
    private AuthorizationServerEndpointsConfigurer myEndpoint;

    // 不相關配置忽略

    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
        endpoints.tokenStore(tokenStore())
                .authorizationCodeServices(authorizationCodeServices())
                .approvalStore(approvalStore())
                .exceptionTranslator(customExceptionTranslator())
                .tokenEnhancer(tokenEnhancerChain())
                .authenticationManager(authenticationManager)
                .userDetailsService(userDetailsService);
        
        this.myEndpoint = endpoints;
    }

    public AuthorizationServerEndpointsConfigurer getEndpoint() {
	return this.myEndpoint;
    }
}

 改造登錄成功處理器

用@Autowired裝載認證中心配置,然後藉助上面的getter方法取得真正的TokenEndpoint,最後根據源碼邏輯endpoint.getTokenGranter().grant(...)方法獲得JWTToken;

@Component
public class MyAuthenticationSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {
    
    @Autowired
    private ClientDetailsService clientDetailsService;
    
    @Autowired
    private AuthenticationServerConfig config;
    
    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication auth)
	    throws IOException, ServletException {
        String clientId = "myclient";
        ClientDetails clientDetails = clientDetailsService.loadClientByClientId(clientId);

        //密碼授權 模式
        Map<String, String> params = new HashMap<String, String>();
        params.put("username", auth.getName());
        params.put("password", auth.getCredentials().toString());
        TokenRequest tokenRequest = new TokenRequest(params, clientId, clientDetails.getScope(), "password");
 
        // 獲取jwt token
        AuthorizationServerEndpointsConfigurer endpoint = config.getEndpoint();
        OAuth2AccessToken token = endpoint.getTokenGranter().grant(tokenRequest.getGrantType(), tokenRequest);
        

        response.setContentType("application/json;charset=UTF-8");
        response.getWriter().write(token2Json(token).toJSONString());
    }
    
    /**
     * 將獲取到的token轉換爲展示的JSON
     * @param token Oauth的token
     */
    private JSONObject token2Json(OAuth2AccessToken token) {
	JSONObject json = new JSONObject();
	
	// 核心信息
	json.put("access_token", token.getValue());
	json.put("token_type", token.getTokenType());
	json.put("refresh_token", token.getRefreshToken().getValue());
	json.put("expires_in", token.getExpiresIn());
	json.put("scope", token.getScope());
	
	// 補充信息
	Map<String, Object> map = token.getAdditionalInformation();
	json.putAll(map);
	
	return json;	
    }
}

補充

爲了使用到密碼模式請求,需要在認證信息中獲知密碼,由於前文【Spring Security】增加RSA密文傳輸登錄提到的RSA加密,我用到的密鑰對是有時效性的,所以認證信息保留RSA加密後的時效密文(當然,能處理完成後去掉是安全的)。密碼擦除的配置在安全中心配置就可以設置,即eraseCredentials設置爲false(如果你的密碼是明文,還是建議先進行加密處理,不然放在認證信息那肯定是不安全的)。

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebServerSecurityConfig extends WebSecurityConfigurerAdapter {
    
    @Autowired
    UserDetailsService userDetailsService;
    
    @Autowired
    private CustomAuthenticationSuccessHandler authenticationSuccessHandler;

    // 不相關配置忽略

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable();
        http.authorizeRequests()
                .anyRequest().authenticated()
                .and()
                .formLogin().permitAll()
                .successHandler(authenticationSuccessHandler);
    }

    @Override
    protected void configure(AuthenticationManagerBuilder authenticationManagerBuilder) throws Exception {
	authenticationManagerBuilder
                .userDetailsService(userDetailsService)
                .passwordEncoder(passwordEncoder())
                .and()
                .authenticationProvider(userAuthenticationProvider())
                .eraseCredentials(false); // 不清除密碼,後續需要手動清除
    }

    
    /**
     * BCrypt加密器
     * @return 加密器
     */
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

}

結論

多翻源碼,要是我直接從/oauth/token這個方法源碼入手,估計早解決了。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章