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 , RedisTemplateredisTemplate ) { RedisClientDetailsService clientDetailsService = new RedisClientDetailsService(dataSource); clientDetailsService.setRedisTemplate(redisTemplate); return clientDetailsService; } @Bean public RandomValueAuthorizationCodeServices authorizationCodeServices(RedisTemplate 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主要配置内容:
例如代码:
@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 { Maprsp =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 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(); } }