为了账号安全,请及时绑定邮箱和手机立即绑定

Spring Security Oauth2 之 核心架构配置

标签:
Java

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            发布订阅模式

原文出处

点击查看更多内容
TA 点赞

若觉得本文不错,就分享一下吧!

评论

作者其他优质文章

正在加载中
  • 推荐
  • 评论
  • 收藏
  • 共同学习,写下你的评论
感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦
今天注册有机会得

100积分直接送

付费专栏免费学

大额优惠券免费领

立即参与 放弃机会
意见反馈 帮助中心 APP下载
官方微信

举报

0/150
提交
取消