【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这个方法源码入手,估计早解决了。

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