Spring Security Oauth2 之 核心架构配置
1 ResourceServerConfigurerAdapter (资源服务器配置)
内部关联了ResourceServerSecurityConfigurer和HttpSecurity。前者与资源安全配置相关,后者与http安全配置相关@Override public void configure(ResourceServerSecurityConfigurer resources) { //resourceId 用于分配给可授予的clientId //stateless 标记以指示在这些资源上仅允许基于令牌的身份验证 //tokenStore token的存储方式(上一章节提到) resources.resourceId(RESOURCE_ID).stateless(true).tokenStore(tokenStore) //authenticationEntryPoint 认证异常流程处理返回 //tokenExtractor token获取方式,默认BearerTokenExtractor // 从header获取token为空则从request.getParameter("access_token") .authenticationEntryPoint(authenticationEntryPoint).tokenExtractor(unicomTokenExtractor); }
其他属性:
accessDeniedHandler 权失败且主叫方已要求特定的内容类型响应
resourceTokenServices 加载 OAuth2Authentication 和 OAuth2AccessToken 的接口
eventPublisher 事件发布-订阅 根据异常的clazz触发不同event
@Configuration @EnableResourceServer protected class ResourceServerConfiguration extends ResourceServerConfigurerAdapter { @Override public void configure(HttpSecurity http) throws Exception { AuthenticationManager oauthAuthenticationManager = oauthAuthenticationManager(http); //OAuth2核心过滤器 resourcesServerFilter = new OAuth2AuthenticationProcessingFilter(); resourcesServerFilter.setAuthenticationEntryPoint(authenticationEntryPoint); //OAuth2AuthenticationManager,只有被OAuth2AuthenticationProcessingFilter拦截到的oauth2相关请求才被特殊的身份认证器处理。 resourcesServerFilter.setAuthenticationManager(oauthAuthenticationManager); if (eventPublisher != null) { //同上 resourcesServerFilter.setAuthenticationEventPublisher(eventPublisher); } if (tokenExtractor != null) { //同上 resourcesServerFilter.setTokenExtractor(tokenExtractor); } resourcesServerFilter = postProcess(resourcesServerFilter); resourcesServerFilter.setStateless(stateless); if (!Boolean.TRUE.toString().equals(apolloCouponConfig.getOauthEnable())) { // 不需要令牌,直接访问资源 http.authorizeRequests().anyRequest().permitAll(); } else { http //.anonymous().disable() //匿名访问 .antMatcher("/**") //匹配需要资源认证路径 .authorizeRequests() .antMatchers("/swagger-ui.html", "/swagger-resources/**", "/v2/api-docs/**", "/validatorUrl","/valid" ).permitAll() //匹配不需要资源认证路径 .anyRequest().authenticated() .and() .addFilterBefore(resourcesServerFilter, AbstractPreAuthenticatedProcessingFilter.class) .exceptionHandling() //添加filter .exceptionHandling().accessDeniedHandler(accessDeniedHandler) //异常处理 .authenticationEntryPoint(authenticationEntryPoint); //认证异常流程 } } }accessDeniedHandler 异常 : 令牌不能访问该资源 (403)异常等
authenticationEntryPoint 异常 : 不传令牌,令牌错误(失效)等
2.AuthorizationServerConfig 认证服务器配置
@Configuration @EnableAuthorizationServer public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter { private static String REALM = "OAUTH_REALM"; /** * 认证管理器,上一篇有涉及到,下面有具体描述 */ @Autowired @Qualifier("authenticationManagerBean") private AuthenticationManager authenticationManager; /** * 获取用户信息 */ @Autowired private UserDetailsService userDetailsService; /** * 加密方式 */ @Autowired private PasswordEncoder passwordEncoder; /** * 数据源 */ @Autowired private DataSource dataSource; /** * 声明 ClientDetails实现 Load a client by the client id. This method must not return null. * @return clientDetails */ @Bean public ClientDetailsService clientDetails() { return new JdbcClientDetailsService(dataSource); } /** * 声明TokenStore实现 * * @return TokenStore */ @Bean public TokenStore tokenStore() { return new JdbcTokenStore(dataSource); } @Bean public AuthorizationCodeServices authorizationCodeServices() { return new JdbcAuthorizationCodeServices(dataSource); } @Bean public ApprovalStore approvalStore(){ return new JdbcApprovalStore(dataSource); } /** * 配置令牌端点(Token Endpoint)的安全约束. * * @param security security * @throws Exception */ @Override public void configure(AuthorizationServerSecurityConfigurer security) throws Exception { security.realm(REALM); security.passwordEncoder(passwordEncoder); security.allowFormAuthenticationForClients(); security.tokenKeyAccess("permitAll()"); security.checkTokenAccess("isAuthenticated()"); } /** * 配置客户端详情服务(ClientDetailsService) * 客户端详情信息在这里进行初始化 * 通过数据库来存储调取详情信息 * * @param clients clients * @throws Exception */ @Override public void configure(ClientDetailsServiceConfigurer clients) throws Exception { clients.withClientDetails(clientDetails()); } /** * 配置授权(authorization)以及令牌(token)的访问端点和令牌服务(token services) * * @param endpoints endpoints * @throws Exception */ @Override public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception { endpoints.authenticationManager(authenticationManager); endpoints.tokenStore(tokenStore()); endpoints.userDetailsService(userDetailsService); endpoints.authorizationCodeServices(authorizationCodeServices()); endpoints.approvalStore(approvalStore()); // 为解决获取token并发问题 DefaultTokenServices tokenServices = new TestDefaultTokenServices(); tokenServices.setTokenStore(endpoints.getTokenStore()); tokenServices.setSupportRefreshToken(true); tokenServices.setClientDetailsService(endpoints.getClientDetailsService()); tokenServices.setTokenEnhancer(endpoints.getTokenEnhancer()); endpoints.tokenServices(tokenServices); } }}
基于JbdcToken,并发操作时会抛异常,加锁解决
@Override public synchronized OAuth2AccessToken createAccessToken(OAuth2Authentication authentication) throws AuthenticationException { return super.createAccessToken(authentication); } @Override public synchronized OAuth2AccessToken refreshAccessToken(String refreshTokenValue, TokenRequest tokenRequest) { return super.refreshAccessToken(refreshTokenValue, tokenRequest); }
因为我这边配置tokenStore方式,是通过dubbo远程RPC暴露(终端控制),因此要配置分布式锁
try{ lock = redisTemplate.opsForValue().setIfAbsent(lockKey, LOCK); logger.info("是否获取到锁:"+lock); if (lock) { // TODO super.createAccessToken(authentication); }else { logger.info("没有获取到锁!"); } }finally{ redisTemplate.delete(lockKey); logger.info("cancelCouponCode任务结束,释放锁!"); }
3. OAuth2AuthenticationProcessingFilter 核心过滤器
OAuth2受保护资源的预认证过滤器。 从传入请求中提取一个OAuth2令牌,并使用它来使用{@link OAuth2Authentication}(如果与OAuth2AuthenticationManager一起使用)填充Spring Security上下文。
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException { final boolean debug = logger.isDebugEnabled(); final HttpServletRequest request = (HttpServletRequest) req; final HttpServletResponse response = (HttpServletResponse) res; try { Authentication authentication = tokenExtractor.extract(request); if (authentication == null) { if (stateless && isAuthenticated()) { if (debug) { logger.debug("Clearing security context."); } SecurityContextHolder.clearContext(); } if (debug) { logger.debug("No token in request, will continue chain."); } } else { request.setAttribute(OAuth2AuthenticationDetails.ACCESS_TOKEN_VALUE, authentication.getPrincipal()); if (authentication instanceof AbstractAuthenticationToken) { AbstractAuthenticationToken needsDetails = (AbstractAuthenticationToken) authentication; needsDetails.setDetails(authenticationDetailsSource.buildDetails(request)); } //身份认证 Authentication authResult = authenticationManager.authenticate(authentication); if (debug) { logger.debug("Authentication success: " + authResult); } //成功事件通知 eventPublisher.publishAuthenticationSuccess(authResult); //保存Security上下文 SecurityContextHolder.getContext().setAuthentication(authResult); } } catch (OAuth2Exception failed) { SecurityContextHolder.clearContext(); if (debug) { logger.debug("Authentication request failed: " + failed); } eventPublisher.publishAuthenticationFailure(new BadCredentialsException(failed.getMessage(), failed), new PreAuthenticatedAuthenticationToken("access-token", "N/A")); authenticationEntryPoint.commence(request, response, new InsufficientAuthenticationException(failed.getMessage(), failed)); return; } chain.doFilter(request, response); }
4.OAuth2AuthenticationManager 认证管理
在上一节源码中有提到,和它的实现类 ProviderManager (未携带access_token)这节认证时候携带 access_token 则跳转 OAuth2AuthenticationManager
public Authentication authenticate(Authentication authentication) throws AuthenticationException { if (authentication == null) { throw new InvalidTokenException("Invalid token (token not found)"); } String token = (String) authentication.getPrincipal(); OAuth2Authentication auth = tokenServices.loadAuthentication(token); if (auth == null) { throw new InvalidTokenException("Invalid token: " + token); } Collection<String> resourceIds = auth.getOAuth2Request().getResourceIds(); if (resourceId != null && resourceIds != null && !resourceIds.isEmpty() && !resourceIds.contains(resourceId)) { throw new OAuth2AccessDeniedException("Invalid token does not contain resource id (" + resourceId + ")"); } checkClientDetails(auth); if (authentication.getDetails() instanceof OAuth2AuthenticationDetails) { OAuth2AuthenticationDetails details = (OAuth2AuthenticationDetails) authentication.getDetails(); // Guard against a cached copy of the same details if (!details.equals(auth.getDetails())) { // Preserve the authentication details from the one loaded by token services details.setDecodedDetails(auth.getDetails()); } } auth.setDetails(authentication.getDetails()); auth.setAuthenticated(true); return auth; }这边的 tokenServices 是资源服务器的 tokenServices 和 上一节的 认证服务器 tokenServices是两个独立的service
认证服务器
public interface AuthorizationServerTokenServices { //创建token OAuth2AccessToken createAccessToken(OAuth2Authentication authentication) throws AuthenticationException; //刷新token OAuth2AccessToken refreshAccessToken(String refreshToken, TokenRequest tokenRequest) throws AuthenticationException; //获取token OAuth2AccessToken getAccessToken(OAuth2Authentication authentication); }资源服务器
public interface ResourceServerTokenServices { //根据accessToken加载客户端信息 OAuth2Authentication loadAuthentication(String accessToken) throws AuthenticationException, InvalidTokenException; //根据accessToken获取完整的访问令牌详细信息。 OAuth2AccessToken readAccessToken(String accessToken); }
5.了解
@Configuration public class WebMvcConfig extends WebMvcConfigurerAdapter { @Bean public LocaleResolver localeResolver() { SessionLocaleResolver slr = new SessionLocaleResolver(); // 默认语言 slr.setDefaultLocale(Locale.US); return slr; } @Bean public LocaleChangeInterceptor localeChangeInterceptor() { LocaleChangeInterceptor lci = new LocaleChangeInterceptor(); // 参数名 lci.setParamName("lang"); return lci; } @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(localeChangeInterceptor()); } @Override public void addViewControllers(ViewControllerRegistry registry) { registry.addViewController("/login").setViewName("login"); registry.addViewController("/oauth/confirm_access").setViewName("authorize"); } }
@Configuration @EnableWebSecurity public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private UserDetailsService userDetailsService; @Bean public PasswordEncoder passwordEncoder() { return new PasswordEncoder(); } @Bean @Override public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); } @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(this.userDetailsService).passwordEncoder(passwordEncoder()); } @Override protected void configure(HttpSecurity http) throws Exception { http // 头部缓存 .headers() .cacheControl() .and() // 防止网站被人嵌套 .frameOptions() .sameOrigin() .and() .csrf() .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()) .and() // 跨域支持 .cors(); http .requestMatchers() //接受的请求 .antMatchers("/login", "/logout", "/oauth/authorize", "/oauth/confirm_access") .and() .authorizeRequests()// 端点排除 .anyRequest().authenticated() .and() .formLogin() .loginPage("/login") .failureUrl("/login?error") .permitAll() .and() .logout() .logoutUrl("/logout") .invalidateHttpSession(true).clearAuthentication(true); } }
endPoint包下提供许多http接口
CheckTokenEndpoint
@RequestMapping(value = "/oauth/check_token") @ResponseBody public Map<String, ?> checkToken(@RequestParam("token") String value) { OAuth2AccessToken token = resourceServerTokenServices.readAccessToken(value); if (token == null) { throw new InvalidTokenException("Token was not recognised"); } if (token.isExpired()) { throw new InvalidTokenException("Token has expired"); } OAuth2Authentication authentication = resourceServerTokenServices.loadAuthentication(token.getValue()); Map<String, ?> response = accessTokenConverter.convertAccessToken(token, authentication); return response; }
补充
涉及到一些设计模式:oAuth2RequestFactory 工厂模式
ResourceServerConfigurerAdapter 适配器模式
AbstractConfiguredSecurityBuilder 建造者模式
TokenStore 模板方法模式
AuthenticationEventPublisher 发布订阅模式