1、前言(单点登录的原理)
我们在做微服务架构中,一般会研发一个认证中心的应用(即一个微服务),其实这就是单点登录(Single Sign on,即SSO)。这个认证中心实现在多系统应用构成的集群中,登录其中任意一个系统,其它系统自动得到授权,从而无需再次登录,从而实现了单点的登录与单点注销两部分。
认证中心是一个独立的应用,即微服务,只有它能接受用户名和密码等安全信息,其他系统不提供登录入口,只接受认证中心的间接授权。间接授权通过令牌实现,so认证中心验证用户的用户名密码没问题,创建授权令牌,在接下来的跳转过程中,授权令牌作为参数发送给各个子系统,子系统拿到令牌,即得到了授权,可以借此创建局部会话,局部会话登录方式与单系统的登录方式相同。这个过程,也就是单点登录的原理。
2、OAuth2的实现,即做一个认证中心,或叫授权服务器的微服务
oauth2通常分为二块:认证服务和资源服务,这二个可以放在同一个应用中,也可以分为作为应用。即可以把认证中心做成一个微服务。
2.1 授权服务的实现
2.1.1.主要复写三个方法:
ClientDetailsServiceConfigurer:这个configurer定义了客户端细节服务。客户详细信息可以被初始化,为了灵活通用客户端的配置信息放在数据库表oauth_client_details(OAuth2自带的),通过jdbc引入,主要字段有:resource_ids(资源id标识),client_id,(相当于AppId)client_secret(即密钥,相当于AppSecret),scope(read/write/trust,多个权限逗号分开),authorized_grant_types(四种认证方式用哪一些),authorities(访问资源所需要的权限)等。
AuthorizationServerSecurityConfigurer:在令牌端点上定义了安全约束,这个一般配合WebSecurityConfig一起使用。
AuthorizationServerEndpointsConfigurer:定义了授权和令牌端点和令牌服务。
/**
* 声明 ClientDetails实现
*/
@Bean
public RedisClientDetailsService redisClientDetailsService(DataSource dataSource , RedisTemplate<String, Object> redisTemplate ) {
RedisClientDetailsService clientDetailsService = new RedisClientDetailsService(dataSource);
clientDetailsService.setRedisTemplate(redisTemplate);
return clientDetailsService;
}
@Bean
public RandomValueAuthorizationCodeServices authorizationCodeServices(RedisTemplate<String, Object> redisTemplate) {
RedisAuthorizationCodeServices redisAuthorizationCodeServices = new RedisAuthorizationCodeServices();
redisAuthorizationCodeServices.setRedisTemplate(redisTemplate);
return redisAuthorizationCodeServices;
}
/**
* 配置身份认证器,配置认证方式,TokenStore,TokenGranter,OAuth2RequestFactory
*/
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
//通用处理
endpoints.tokenStore(tokenStore).authenticationManager(authenticationManager)
// 支持
.userDetailsService(userDetailsService);
if(tokenStore instanceof JwtTokenStore){
endpoints.accessTokenConverter(jwtAccessTokenConverter);
}
//处理授权码
endpoints.authorizationCodeServices(authorizationCodeServices);
// 处理 ExceptionTranslationFilter 抛出的异常
endpoints.exceptionTranslator(webResponseExceptionTranslator);
}
/**
* 配置应用名称 应用id
* 配置OAuth2的客户端相关信息
*/
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.withClientDetails(redisClientDetailsService);
redisClientDetailsService.loadAllClientToCache();
}
/**
* 对应于配置AuthorizationServer安全认证的相关信息,创建ClientCredentialsTokenEndpointFilter核心过滤器
*/
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
// url:/oauth/token_key,exposes
security.tokenKeyAccess("permitAll()")
/// public key for token
/// verification if using
/// JWT tokens
// url:/oauth/check_token
.checkTokenAccess("isAuthenticated()")
// allow check token
.allowFormAuthenticationForClients();
}
2.1.2.开启spring security,即自定义WebSecurityConfigurerAdapter。
2.1.3. 自定义用户认证的实现:
认证是由AuthenticationManager 来管理的,即自定义AuthenticationProvider。注意AuthenticationManager 中可以定义有多个 AuthenticationProvider。
2.1.4 自定义UserDetailsService
自定义需要实现UserDetailsService接口,并且重写loadUserByUsername方法。返回的用户信息需要实现UserDatails接口。
2.2资源服务的实现
ResourceServerSecurityConfigurer主要配置内容:
- tokenServices:ResourceServerTokenServices 类的实例,用来实现令牌业务逻辑服务
- resourceId:这个资源服务的ID,这个属性是可选的,但是推荐设置并在授权服务中进行验证
- tokenExtractor 令牌提取器用来提取请求中的令牌
- 请求匹配器,用来设置需要进行保护的资源路径,默认的情况下是受保护资源服务的全部路径
- 受保护资源的访问规则,默认的规则是简单的身份验证(plain authenticated)
- 其他的自定义权限保护规则通过 HttpSecurity 来进行配置
例如代码:
@Configuration
@EnableResourceServer
@EnableConfigurationProperties(PermitUrlProperties.class)
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
@Autowired
private PermitUrlProperties permitUrlProperties;
@Autowired(required = false)
private TokenStore tokenStore;
@Autowired
private ObjectMapper objectMapper ; //springmvc启动时自动装配json处理类
@Autowired
private OAuth2WebSecurityExpressionHandler expressionHandler;
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/health");
web.ignoring().antMatchers("/oauth/user/token");
web.ignoring().antMatchers("/oauth/client/token");
}
@Override
public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
if (tokenStore != null) {
resources.tokenStore(tokenStore);
}
resources.stateless(true);
resources.expressionHandler(expressionHandler);
// 自定义异常处理端口
resources.authenticationEntryPoint(new AuthenticationEntryPoint() {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response,
AuthenticationException authException) throws IOException, ServletException {
Map<String ,String > rsp =new HashMap<>();
response.setStatus(HttpStatus.UNAUTHORIZED.value() );
rsp.put("resp_code", HttpStatus.UNAUTHORIZED.value() + "") ;
rsp.put("resp_msg", authException.getMessage()) ;
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write(objectMapper.writeValueAsString(rsp));
response.getWriter().flush();
response.getWriter().close();
}
});
resources.accessDeniedHandler(new OAuth2AccessDeniedHandler(){
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException authException) throws IOException, ServletException {
Map<String ,String > rsp =new HashMap<>();
response.setContentType("application/json;charset=UTF-8");
response.setStatus(HttpStatus.UNAUTHORIZED.value() );
rsp.put("resp_code", HttpStatus.UNAUTHORIZED.value() + "") ;
rsp.put("resp_msg", authException.getMessage()) ;
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write(objectMapper.writeValueAsString(rsp));
response.getWriter().flush();
response.getWriter().close();
}
});
}
@Override
public void configure(HttpSecurity http) throws Exception {
http.requestMatcher(
/**
* 判断来源请求是否包含oauth2授权信息
*/
new RequestMatcher() {
private AntPathMatcher antPathMatcher = new AntPathMatcher();
@Override
public boolean matches(HttpServletRequest request) {
// 请求参数中包含access_token参数
if (request.getParameter(OAuth2AccessToken.ACCESS_TOKEN) != null) {
return true;
}
// 头部的Authorization值以Bearer开头
String auth = request.getHeader(UaaConstant.AUTHORIZTION);
if (auth != null) {
if (auth.startsWith(OAuth2AccessToken.BEARER_TYPE)) {
return true;
}
}
// 认证中心url特殊处理,返回true的,不会跳转login.html页面
if (antPathMatcher.match(request.getRequestURI(), "/api-auth/oauth/userinfo")) {
return true;
}
if (antPathMatcher.match(request.getRequestURI(), "/api-auth/oauth/remove/token")) {
return true;
}
if (antPathMatcher.match(request.getRequestURI(), "/api-auth/oauth/get/token")) {
return true;
}
if (antPathMatcher.match(request.getRequestURI(), "/api-auth/oauth/refresh/token")) {
return true;
}
if (antPathMatcher.match(request.getRequestURI(), "/api-auth/oauth/token/list")) {
return true;
}
if (antPathMatcher.match("/**/clients/**", request.getRequestURI())) {
return true;
}
if (antPathMatcher.match("/**/services/**", request.getRequestURI())) {
return true;
}
if (antPathMatcher.match("/**/redis/**", request.getRequestURI())) {
return true;
}
return false;
}
}
).authorizeRequests().antMatchers(permitUrlProperties.getIgnored()).permitAll()
.requestMatchers(EndpointRequest.toAnyEndpoint()).permitAll()
.anyRequest()
.authenticated();
}
}