背景
由於前後端分離的原因,在使用默認的表單登錄時,希望能像密碼模式一樣直接返回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這個方法源碼入手,估計早解決了。