项目架构图:

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

先从第二步开始(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);
// Oauth2的 密码授权模式
Map<String, String> map = Maps.newHashMap();
// 获取数据库配置的 clientId
String clientId = ${clientId};
map.put("username", username);
map.put("password", password);
map.put("grant_type", "password");
map.put("scope", "read write trust"); // 权限共有 read write trust 三种
// 构建客户端的Authentication
UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(
clientId, "", Lists.newArrayList());
// 调用Spring security Oauth2 的postAccessToken 生成 Oauth2AccessToken
ResponseEntity<OAuth2AccessToken> responseEntity = tokenEndpoint
.postAccessToken(token, map);
OAuth2AccessToken oAuth2AccessToken = responseEntity.getBody();
JwtAuthenticationToken authenticationToken = null;
if (oAuth2AccessToken != null) {
// 对生成的token包装成Jwt的实现, tokenProvider是自己写的一个接口,共有两个方法,下文会分析
authenticationToken = tokenProvider.createToken(request, response,
oAuth2AccessToken);
}
// 把一些不敏感的信息提取出来
Map<String, Object> oauthInfo = extract(authenticationToken);
// 存入redis
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("")) {
// Only validate the client details if a client authenticated during this
// request.
if (!clientId.equals(tokenRequest.getClientId())) {
// double check to make sure that the client ID in the token request is the same as that in the
// authenticated client
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)) {
// The scope was requested or determined during the authorization step
if (!tokenRequest.getScope().isEmpty()) {
logger.debug("Clearing scope of incoming token request");
tokenRequest.setScope(Collections.<String> emptySet());
}
}
if (isRefreshTokenRequest(parameters)) {
// A refresh token has its own default scopes, so we should ignore any added by the factory here.
tokenRequest.setScope(OAuth2Utils.parseParameterList(parameters.get(OAuth2Utils.SCOPE)));
}
// 有五种类型的Granter,我司只实现了两种。
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();
// 登录用户授权模式 password,authorization_code,refresh_token
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());
// USERINFO_FIELDS_CACHE 常量是UserDetail的所有属性集合
for (Field field : USERINFO_FIELDS_CACHE) {
// 找寻所有的get方法
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法则设计的业务表。这里看看流程即可。学的只是个思想。