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 發布訂閱模式