背景
由于前后端分离的原因,在使用默认的表单登录时,希望能像密码模式一样直接返回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这个方法源码入手,估计早解决了。