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 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 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 response = accessTokenConverter.convertAccessToken(token, authentication);
return response;
}
补充
涉及到一些设计模式:
oAuth2RequestFactory 工厂模式
ResourceServerConfigurerAdapter 适配器模式
AbstractConfiguredSecurityBuilder 建造者模式
TokenStore 模板方法模式
AuthenticationEventPublisher 发布订阅模式