SpringSecurityOAuth核心源码解析
蓝色表示接口,绿色表示类
1,TokenEndpoint 整个入口点,相当于一个controller,不同的授权模式获取token的地址都是 /oauth/token ,通过grant_type 参数标识不同的授权类型,这个类就是判断授权类型 grant_type的。
2,TokenEndpoint收到请求后先调用 ClientDetailsService 读取第三方应用的信息。client_id、client_secret 来获取不同的第三方应用信息,封装在ClientDetails里;TokenEndpoint还会创建一个TokenRequest 的对象 ,封装了请求grant_type等信息,将ClientDetails放在TokenRequest 里
3,TokenRequest 会调用TokenGranter (令牌授权者)四种授权模式的实现,根据请求的grant_type 挑一种令牌生成的逻辑,都会产生OAuth2Request(整合了ClientDetails+TokenRequest) 、 Authentication(封装授权用户的信息,通过UserDetailsService获取),这两个对象整合起来生成 OAuth2Authentication 对象(包装了哪个第三方应用在请求哪个用户授权,授权模式,授权参数等信息),
4,OAuth2Authentication 最后传给AuthorizationServerTokenServices(认证服务器令牌服务)的默认实现,生成一个令牌。AuthorizationServerTokenServices 里还有TokenStore用来处理令牌的存取,TokenEnhancer 令牌增强器。
重构三种登录方式
要做的就是在登录成功后,按照Spring Security OAuth 生成 access_token 的逻辑,生成自己的token。
分析:
逆着推,要想有OAuth2AccessToken ------>AuthorizationServerTokenServices(spring已提供默认实现)------>OAuth2Authentication对象--------->Authentication(登录成功后会有这个对象)、OAuth2Request。
所以只需要构建OAuth2Request即可。
OAuth2Request-------->ClientDetails( 封装第三方应用的信息)------------>ClientDetailsService------------>接收ClientId 构建ClientDetails-------------->请求参数
--------->TokenRequest-------->根据ClientDetails信息new出来
此部分代码都是app项目里的
其中一部分是从BasicAuthenticationFilter 类里copy一段代码,这个类是处理Basic登录用的,copy一段获取请求头信息的
ImoocAuthenticationSuccessHandler:
/** * 认证成功后做的处理 * ClassName: ImoocAuthenticationSuccessHandler * @Description: 认证成功后做的处理 * @author lihaoyang * @date 2018年3月1日 */ @Component("imoocAuthenticationSuccessHandler") public class ImoocAuthenticationSuccessHandler //spring默认的登录成功处理器,实现了AuthenticationSuccessHandler接口,适配登录后 重定向和返回json两种用这个实现 extends SavedRequestAwareAuthenticationSuccessHandler //返回json用这个实现 /*implements AuthenticationSuccessHandler*/{ private Logger logger = LoggerFactory.getLogger(getClass()); //springmvc启动会自动注册一个ObjectMapper @Autowired private ObjectMapper objectMapper; @Autowired private SecurityProperties securityProperties; @Autowired private ClientDetailsService clientDetailsService; @Autowired private AuthorizationServerTokenServices authorizationServerTokenServices; @Override public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException { logger.info("登录成功"); //获取请求头里Authorization信息 String header = request.getHeader("Authorization"); //没有client信息 if (header == null || !header.startsWith("Basic ")) { throw new UnapprovedClientAuthenticationException("请求头中无client信息"); } /** * 构造OAuth2Request 第一步,从请求头获取clientId */ //base64解码获取clientId、clientSecret String[] tokens = extractAndDecodeHeader(header, request); assert tokens.length == 2; String clientId = tokens[0]; String clientSecret = tokens[1]; /** * 构造OAuth2Request 第二步,根据clientId 获取ClientDetails对象 */ ClientDetails clientDetails = clientDetailsService.loadClientByClientId(clientId); if(clientDetails == null){ throw new UnapprovedClientAuthenticationException("clientId对应的配置信息不存在,clientId:"+clientId); }else if(!StringUtils.equals(clientDetails.getClientSecret(), clientSecret)){ throw new UnapprovedClientAuthenticationException("clientSecret不匹配,clientId:"+clientId); } /** * 构造OAuth2Request 第三步,new TokenRequest * 第一个参数是个map,放的是每个授权模式特有的参数,springsecurity会根据这些参数构建Authentication * 我们这里已获取到了Authentication,弄个空的map就可 */ TokenRequest tokenRequest = new TokenRequest(MapUtils.EMPTY_MAP, clientId, clientDetails.getScope(), "custom"); /** * 构造OAuth2Request 第四步,创建 OAuth2Request */ OAuth2Request oAuth2Request = tokenRequest.createOAuth2Request(clientDetails); /** * 构造OAuth2Request 第五步,创建 OAuth2Authentication */ OAuth2Authentication oAuth2Authentication = new OAuth2Authentication(oAuth2Request, authentication); /** * 构造OAuth2Request 第六步,authorizationServerTokenServices创建 OAuth2AccessToken */ OAuth2AccessToken accessToken = authorizationServerTokenServices.createAccessToken(oAuth2Authentication); response.setContentType("application/json;charset=UTF-8"); response.getWriter().write(objectMapper.writeValueAsString(accessToken)); } /** * base64解码请求头 Basic aW1vb2M6aW1vb2NzZWNyZXQ= * Decodes the header into a username and password. * * @throws BadCredentialsException if the Basic header is not present or is not valid * Base64 */ private String[] extractAndDecodeHeader(String header, HttpServletRequest request) throws IOException { //Basic aW1vb2M6aW1vb2NzZWNyZXQ= 截取Basic后的 byte[] base64Token = header.substring(6).getBytes("UTF-8"); byte[] decoded; try { //解码后格式 用户名:密码 decoded = Base64.decode(base64Token); } catch (IllegalArgumentException e) { throw new BadCredentialsException( "Failed to decode basic authentication token"); } String token = new String(decoded, "UTF-8"); int delim = token.indexOf(":"); if (delim == -1) { throw new BadCredentialsException("Invalid basic authentication token"); } //返回的数组是 [用户名(就是client_id),clientSecret] 其实就是配置的 /** * security.oauth2.client.clientId = imooc security.oauth2.client.clientSecret = imoocsecret */ return new String[] { token.substring(0, delim), token.substring(delim + 1) }; } }
ImoocResourceServerConfig:安全配置
/** * 资源服务器,和认证服务器在物理上可以在一起也可以分开 * ClassName: ImoocResourceServerConfig * @Description: TODO * @author lihaoyang * @date 2018年3月13日 */ @Configuration @EnableResourceServer public class ImoocResourceServerConfig extends ResourceServerConfigurerAdapter{ //自定义的登录成功后的处理器 @Autowired private AuthenticationSuccessHandler imoocAuthenticationSuccessHandler; //自定义的认证失败后的处理器 @Autowired private AuthenticationFailureHandler imoocAuthenticationFailureHandler; //读取用户配置的登录页配置 @Autowired private SecurityProperties securityProperties; @Autowired private SmsCodeAuthenticationSecurityConfig smsCodeAuthenticationSecurityConfig; @Override public void configure(HttpSecurity http) throws Exception { http //----------表单认证相关配置--------------- .formLogin() .loginPage(SecurityConstants.DEFAULT_UNAUTHENTICATION_URL) //处理用户认证BrowserSecurityController .loginProcessingUrl(SecurityConstants.DEFAULT_LOGIN_PROCESSING_URL_FORM) .successHandler(imoocAuthenticationSuccessHandler)//自定义的认证后处理器 .failureHandler(imoocAuthenticationFailureHandler) //登录失败后的处理 .and() //-----------授权相关的配置 --------------------- .authorizeRequests() // /authentication/require:处理登录,securityProperties.getBrowser().getLoginPage():用户配置的登录页 .antMatchers(SecurityConstants.DEFAULT_UNAUTHENTICATION_URL, securityProperties.getBrowser().getLoginPage(),//放过登录页不过滤,否则报错 SecurityConstants.DEFAULT_LOGIN_PROCESSING_URL_MOBILE, SecurityConstants.SESSION_INVALID_PAGE, SecurityConstants.DEFAULT_VALIDATE_CODE_URL_PREFIX+"/*").permitAll() //验证码 .anyRequest() //任何请求 .authenticated() //都需要身份认证 .and() .csrf().disable() //关闭csrf防护 .apply(smsCodeAuthenticationSecurityConfig);//把短信验证码配置应用上 } }
启动demo项目,demo项目此时依赖app项目,此时做的是表单登录的处理
访问表单登录:添加Authorization信息,username配置的cilentId:imooc ,password配置的clientSecret:imoocsecret
#第三方应用client_id
security.oauth2.client.clientId = imooc
#注意clientSecret 大写
security.oauth2.client.clientSecret = imoocsecret
响应token:
{
"access_token": "f75ee2e8-a20b-4790-a3b9-64198766dc64",
"token_type": "bearer",
"refresh_token": "013f25fa-affc-445e-bdf6-2adb021aa60d",
"expires_in": 43199
}
拿着token 访问用户信息请求 http://127.0.0.1:8080/user/me,header加上
响应:
{ "password": null, "username": "user", "authorities":[ { "authority": "ROLE_USER" }, { "authority": "admin" } ], "accountNonExpired": true, "accountNonLocked": true, "credentialsNonExpired": true, "enabled": true, "userId": "user" }
至此可以通过通过用户名密码获取token,拿着token访问用户信息了!
具体代码放在了github https://github.com/lhy1234/spring-security
欢迎关注个人公众号一起交流学习: