项目架构图:

这里只写我司的认证中心设计,先从认证流程图开始:

先从第二步开始(Oauth2的认证 /oauth/login 接口):
/oauth/login 资源接口
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
| public Response<Map<String, Object>> login(String username, String password, HttpServletRequest request, HttpServletResponse response) throws HttpRequestMethodNotSupportedException { Asserts.notEmpty(username); Asserts.notEmpty(password); Map<String, String> map = Maps.newHashMap(); String clientId = ${clientId}; map.put("username", username); map.put("password", password); map.put("grant_type", "password"); map.put("scope", "read write trust"); UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken( clientId, "", Lists.newArrayList()); ResponseEntity<OAuth2AccessToken> responseEntity = tokenEndpoint .postAccessToken(token, map); OAuth2AccessToken oAuth2AccessToken = responseEntity.getBody(); JwtAuthenticationToken authenticationToken = null; if (oAuth2AccessToken != null) { authenticationToken = tokenProvider.createToken(request, response, oAuth2AccessToken); } Map<String, Object> oauthInfo = extract(authenticationToken); tokenRedisService.putOauthInfo(authenticationToken.getAccessToken(),oauthInfo); return Response.ok(oauthInfo); }
|
这里首先构建了一个UsernamePasswordAuthenticationToken类型的Authentication.然后调用TokenPoint的postAccessToken方法去创建一个Oauth2AccessToken, 接下来通过Oauth2AccessToken创建一个JwtAuthenticationToken, 在创建JwtAuthentiationToken的时候就会将access_token等信息写入客户端Cookie, 再从token里面取一些不敏感信息返回。
TokenEnpoint的PostAccessToken方法(Spring 官方实现)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52
| @RequestMapping(value = "/oauth/token", method=RequestMethod.POST) public ResponseEntity<OAuth2AccessToken> postAccessToken(Principal principal, @RequestParam Map<String, String> parameters) throws HttpRequestMethodNotSupportedException { if (!(principal instanceof Authentication)) { throw new InsufficientAuthenticationException( "There is no client authentication. Try adding an appropriate authentication filter."); } String clientId = getClientId(principal); ClientDetails authenticatedClient = getClientDetailsService().loadClientByClientId(clientId); TokenRequest tokenRequest = getOAuth2RequestFactory().createTokenRequest(parameters, authenticatedClient); if (clientId != null && !clientId.equals("")) { if (!clientId.equals(tokenRequest.getClientId())) { throw new InvalidClientException("Given client ID does not match authenticated client"); } } if (authenticatedClient != null) { oAuth2RequestValidator.validateScope(tokenRequest, authenticatedClient); } if (!StringUtils.hasText(tokenRequest.getGrantType())) { throw new InvalidRequestException("Missing grant type"); } if (tokenRequest.getGrantType().equals("implicit")) { throw new InvalidGrantException("Implicit grant type not supported from token endpoint"); } if (isAuthCodeRequest(parameters)) { if (!tokenRequest.getScope().isEmpty()) { logger.debug("Clearing scope of incoming token request"); tokenRequest.setScope(Collections.<String> emptySet()); } } if (isRefreshTokenRequest(parameters)) { tokenRequest.setScope(OAuth2Utils.parseParameterList(parameters.get(OAuth2Utils.SCOPE))); } OAuth2AccessToken token = getTokenGranter().grant(tokenRequest.getGrantType(), tokenRequest); if (token == null) { throw new UnsupportedGrantTypeException("Unsupported grant type: " + tokenRequest.getGrantType()); } return getResponse(token); }
|
接下来将第一步的实现。因为每个服务都是一个资源服务器,所以在资源服务器的配置文件里配置了对应的信息:
资源服务器的配置文件(省略部分信息):
1 2 3 4 5 6 7 8 9
| @Override public void configure(ResourceServerSecurityConfigurer resources) throws Exception { resources.resourceId(RESOURCE_ID) .accessDeniedHandler(new ThrowAccessDeniedHandler(objectMapper)) .authenticationEntryPoint(new ThrowEntryPoint(objectMapper)) .stateless(true) .tokenServices(new UAALoadBalancerUserInfoTokenServices(loadBalancerClient,resourceServerProperties.getServiceId(), resourceServerProperties.getUserInfoUri())); }
|
这里是配置获取用户信息的地址(userInfoUrl).和官方的UserInfoTokenServices不同的是这里用的是从注册中心获取认证中心的实例地址,用到了Ribbon的LoadBalancerClient来根据serviceId获取认证中心的物理地址。核心的getMap方法实现如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| protected Response getMap(String path, String accessToken) { if (LOGGER.isDebugEnabled()) { LOGGER.debug("Getting user info from: " + path); } try { OAuth2RestOperations restTemplate = this.restTemplate; if (restTemplate == null) { BaseOAuth2ProtectedResourceDetails resource = new BaseOAuth2ProtectedResourceDetails(); resource.setClientId(this.clientId); restTemplate = new OAuth2RestTemplate(resource); } OAuth2AccessToken existingToken = ((OAuth2RestOperations)restTemplate).getOAuth2ClientContext().getAccessToken(); if (existingToken == null || !accessToken.equals(existingToken.getValue())) { DefaultOAuth2AccessToken token = new DefaultOAuth2AccessToken(accessToken); token.setTokenType(this.tokenType); ((OAuth2RestOperations)restTemplate).getOAuth2ClientContext().setAccessToken(token); } return (Response)((OAuth2RestOperations)restTemplate).getForEntity(path, Response.class, new Object[0]).getBody(); } catch (Exception var6) { LOGGER.warn("Could not fetch user details: " + var6.getClass() + ", " + var6.getMessage()); return null; } } }
|
和官方的实现不同就是返回值不同。接下来再看认证中心对应的接口实现(/oauth/me):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56
| @RequestMapping(method = RequestMethod.GET, value ="/oauth/me") public Map<String, Object> me( @PathVariable(required = false) String service, OAuth2Authentication auth) { Map<String, Object> me = Collections.EMPTY_MAP; List<SimpleGrantedAuthorityImpl> authorities = new ArrayList<SimpleGrantedAuthorityImpl>(); logger.info( "/oauth/me request... OAuth2Authentication[{}] service[{}]", auth, service); if (auth == null) { throw new OAuth2Exception("Authorization message is not null."); } OAuth2Request request = auth.getOAuth2Request(); if (request == null) { me.clear(); throw new OAuth2Exception("OAuth2Request message is not null."); } Authentication authentication = auth.getUserAuthentication(); if (authentication instanceof UsernamePasswordAuthenticationToken) { UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = (UsernamePasswordAuthenticationToken) authentication; UserDetails userInfo = userService .selectByUserNameOrMobileOrEmail(usernamePasswordAuthenticationToken .getName()); userInfo.setLoginPasswd(""); me.clear(); userInfo.setAuthorities(authorities); me.put("name", userInfo.getLoginName()); for (Field field : USERINFO_FIELDS_CACHE) { Method m = ReflectionUtils.findMethod(userInfo.getClass(), SecurityUtils.getMethodName(field.getName())); if (null != m) { me.put(field.getName(), ReflectionUtils.invokeMethod(m, userInfo)); } } return Response.ok(me); } me.clear(); throw new OAuth2Exception("Bad request."); }
|
在认证中心创建token时对token还进行了部分操作,这里就不细说了。数据库表设计的话是Oauth2官方提供的那个Schema加上根据Rbac法则设计的业务表。这里看看流程即可。学的只是个思想。