微服务认证方案设计之spring-security-oauth(二)

上一章节中,我们介绍了大体的概念。这一章节将主要讲解oauth2.0 + jwt + spring security的开发知识

整体技术框架

  • 认证中心(授权服务器): spring security oauth 2.1.2.RELEASE
  • 鉴权框架: spring security 5.3.8.RELEASE
  • 认证协议: oauth2.0
  • 无状态令牌: jwt

认证中心框架选择

方案一: 升级现有的cas4.3至cas6.x版本:

总体上来说,6.x版本cas从功能上来说是满足我们的要求的,但是cas6.x有以下不足,让我最终放弃了使用它:

  • 采用了gradle作为包管理器,而项目组现在只用maven,
  • 6.x版本的cas将包拆分的过于细了,导致阅读与寻找源码并不方便,
  • cas6.x官方并不推荐也不支持你去修改它的登录流程,自定义流程太麻烦了

综上,cas6.x适合那些登录流程简单较为固定,需要开箱即用的项目,不适合那些需要自定义修改很多内容的项目。对于企业应用来说,它推荐的使用方式是war包overlay部署方式,里面强塞了太多不需要的东西,太沉重了

还有一点,6.x版本的cas中文文档与博客极少,遇到问题只能看源码,比较痛苦。

方案二: 采用spring-security-oauth

虽然spring-security-oauth已经进入了维护阶段,后续官方将废弃该项目,现在spring官方已经将spring-security-oauth中的大部分功能(除了认证服务器)合并入了spring-security中统一管理,后续官方将为认证服务器单独推出一个全新的项目: spring-authorization-server。它目前还处于实验阶段,所以不敢采用。

而spring-security-oauth作为认证中心,虽然已经进入了停止更新状态,但官方依然会维护一段时间。并且作为一个很成熟的spring框架,它预留了很多的方法来供我们自定义,修改起来非常方便。并且设计上也很简单清爽,便于阅读,中文文档和博客也是很多的,所以我采用了这个框架作为我们的认证中心。

鉴权框架选择

shiro 和 spring-security在功能上都是很好的框架,但Shiro 最大的问题在于和 Spring 家族的产品进行整合的时候非常不便,对比之下,虽然spring-security相对Shiro会复杂很多,但基于 Spring Boot/Spring Cloud 的微服务项目基本上是原生支持spring-security的,对接起来非常方便。因此选择spring-security。

认证协议与令牌方案

oauth2.0+jwt, oauth2.0只是一种认证协议,具体的令牌方案目前最流行的就是自解析的jwt,很适合微服务方案。

问题: 假如微服务框架使用非自解析的令牌,相对于jwt有什么优势又有什么劣势呢?


==优势: 安全性更高,对令牌的控制更方便 劣势: 后端服务器压力大,一次认证流程中会有更多的http请求==

具体技术讲解

oauth2.0与jwt非常简单,这里就不仔细讲了。主要说一下认证中心与鉴权框架。

demo演示(几种模式及其参数解释)

这里推荐一个个人认为很好的demo, 地址如下

这个项目拉下来直接就可以跑,不需要配置任何数据库,按照readme中的提示一步步跑起来,然后主要要体会一下它的几种模式的调用和弄清楚它各个模式里面的参数都代表什么意思,代码原理倒不怎么用看

  • 密码模式

密码模式请求的是 /oauth/token 这个接口,一共有五个参数:

参数名 位置 作用 说明
username Authorization中Type为Basic Auth的username选项 对应于oauth服务端中注册好的clientId 无论是哪个客户端接入都必须提前注册clientId和clientSecret
password Authorization中Type为Basic Auth的password选项 对应于oauth服务端中注册好的clientSecret 同上
username Body中 用户名
password Body中 密码
grant_type Body中 授权类型 这个参数决定你调用的是哪种模式,密码模式为password
  • 授权码模式

第一步,浏览器访问这个地址 http://localhost:8001/oauth/authorize?response_type=code&scope=sever&client_id=yaohw&redirect_uri=http://www.baidu.com&state=0583 然后会重定向到登录页

参数名 解释
response_type 授权码模式固定为code
scope 请求的token的作用范围
client_id 在oauth2认证中心注册的客户端Id
redirect_uri 登录成功后的重定向地址,该地址必须与在oauth2认证中心注册的重定向地址完全一致
state 自定义参数,忽略

第二步,在页面输入用户名和密码,就会带着授权码?code=xxx跳转到上面哪个地址,将授权码拷贝下来

第三步,post访问http://localhost:8001/oauth/token,参数如下

访问/oauth/token时候,请求头Authorization中都必须配置Type为Basic Auth,然后将clientId和clientSecret设入,后面就不讲解这个了

参数名 解释
grant_type 这里对应获取token的模式,授权码模式必须为authorization_code
code 上一步获取的授权码
redirect_uri 登录成功后的重定向地址,该地址必须与在oauth2认证中心注册的重定向地址完全一致

下面三种模式就不多说了,你们可以仔细观察下调用的几个接口的区别,可以发现以下几点区别

只要是前端页面跳转,都是调用http://localhost:8001/oauth/authorize?response_type=这个地址,并且是通过response_type这个参数来区分模式

而如果是后端接口调用,则是通过http://localhost:8001/oauth/token这个接口来调用,并且通过grant_type这个字段来区分模式,并且一定带有clientId和clientSecret

  • 自定义手机验证码模式(这个可以忽略,需要搞redis,是作者自定义的,不嫌麻烦也可以试下)
  • 简化模式
  • 刷token模式

从接口开始走一遍各个模式的流程

因为上面我们发现oauth2.0的四种模式,其实就对应着两个地址/token和/authorize.因此接下来,我们就过一下着两个url对应的入口TokenEndpoint和AuthorizationEndpoint

AuthorizationEndpoint

对应认证码模式和纯前端的简化模式,具体实现类为org.springframework.security.oauth2.provider.endpoint.AuthorizationEndpoint

总体入口在122行的authorize方法这里,下面我会把没用的代码都删掉,通过注释带大家看一遍这个方法

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
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
@RequestMapping(value = "/oauth/authorize")
public ModelAndView authorize(Map<String, Object> model, @RequestParam Map<String, String> parameters,
SessionStatus sessionStatus, Principal principal) {

//通过工厂类将参数包裹为authorizationRequest
AuthorizationRequest authorizationRequest = getOAuth2RequestFactory().createAuthorizationRequest(parameters);

//校验该方法中的response_type参数
Set<String> responseTypes = authorizationRequest.getResponseTypes();
if (!responseTypes.contains("token") && !responseTypes.contains("code")) {
throw new UnsupportedResponseTypeException("Unsupported response types: " + responseTypes);
}

//校验是否提供clientId
if (authorizationRequest.getClientId() == null) {
throw new InvalidClientException("A client id must be provided");
}

try {

/**
这里非常重要!!!假如这里你没有登录,这里会直接抛出AuthenticationException,
而这个异常会被过滤器链filterChain上的spring security 专用的异常处理过滤器捕捉,
对应的是org.springframework.security.web.access.ExceptionTranslationFilter的105行
的handleSpringSecurityException这个方法,捕捉后根据不同的实现类
spring会执行不同的操作,对应于认证码模式,它就会重定向走

因此假如你的项目有全局异常处理器,这里一定要记得不要去捕捉所有异常,如果
把spring security的异常捕捉了,就会产生意料外的缺陷
**/
if (!(principal instanceof Authentication) || !((Authentication) principal).isAuthenticated()) {
throw new InsufficientAuthenticationException(
"User must be authenticated with Spring Security before authorization can be completed.");
}

//这里通过用户传来的clientId去后端查这个客户端的信息,至于具体是去哪里查,
//依赖于用户具体的service实现方式是jdbc还是redis
ClientDetails client = getClientDetailsService().loadClientByClientId(authorizationRequest.getClientId());

//解析并判断redirect_uri中指定的参数是否与后端配置的一样
String redirectUriParameter = authorizationRequest.getRequestParameters().get(OAuth2Utils.REDIRECT_URI);
String resolvedRedirect = redirectResolver.resolveRedirect(redirectUriParameter, client);
if (!StringUtils.hasText(resolvedRedirect)) {
throw new RedirectMismatchException(
"A redirectUri must be either supplied or preconfigured in the ClientDetails");
}
authorizationRequest.setRedirectUri(resolvedRedirect);

//检查scope参数是否与后端配置的一样
oauth2RequestValidator.validateScope(authorizationRequest, client);

//这里是用来判断当你采用认证码模式时,跳转过去后,是否需要自动批准第三方客户端使用你的某些信息
authorizationRequest = userApprovalHandler.checkForPreApproval(authorizationRequest,
(Authentication) principal);
boolean approved = userApprovalHandler.isApproved(authorizationRequest, (Authentication) principal);
authorizationRequest.setApproved(approved);

//假如你点了批准或者设置为默认批准
if (authorizationRequest.isApproved()) {
//如果你的response_type是token,则这里直接返回token给前端
if (responseTypes.contains("token")) {
return getImplicitGrantResponse(authorizationRequest);
}
//response_type为code,这里生成code后,return一个重定向的modelAndView完成跳转
if (responseTypes.contains("code")) {
return new ModelAndView(getAuthorizationCodeResponse(authorizationRequest,
(Authentication) principal));
}
}

// Store authorizationRequest AND an immutable Map of authorizationRequest in session
// which will be used to validate against in approveOrDeny()
model.put(AUTHORIZATION_REQUEST_ATTR_NAME, authorizationRequest);
model.put(ORIGINAL_AUTHORIZATION_REQUEST_ATTR_NAME, unmodifiableMap(authorizationRequest));

return getUserApprovalPageResponse(model, authorizationRequest, (Authentication) principal);
}
catch (RuntimeException e) {
sessionStatus.setComplete();
throw e;
}

总结一下,这个方法对登录和未登录的访问会进行以下两种处理流程:

  • 假如你是未登录来访问这个接口,那么会抛出异常,被ExceptionTranslationFilter捕获,调用handleSpringSecurityException方法,进入sendStartAuthentication方法,假如你配置的是重定向地址,他这里应该就会找到这个LoginUrlAuthenticationEntryPoint,给个重定向到登录页,让你登录。
  • 当你登录server之后,再进这里就是已登录状态了,就直接过去了,然后给你生成code重定向走
TokenEndpoint

对应密码模式和纯后端的client_credentials模式以及拿着code请求token的认证码模式的第二步,具体实现类为org.springframework.security.oauth2.provider.endpoint.TokenEndpoint

总体入口在87行的postAccessToken方法这里,下面我会把没用的代码都删掉,通过注释带大家看一遍这个方法

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
57
58
59
60
@RequestMapping(value = "/oauth/token", method=RequestMethod.POST)
public ResponseEntity<OAuth2AccessToken> postAccessToken(Principal principal, @RequestParam
Map<String, String> parameters) throws HttpRequestMethodNotSupportedException {

//在到达这个入口之前,UsernamePasswordAuthenticationFilter会
//把Authorization头中的clientId和clientSecret取出来,放到principal中。
//因此下面这段主要是对clientId和clientSecret进行校验
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())) {
//如果不相等或者没找到,则直接抛出异常
throw new InvalidClientException("Given client ID does not match authenticated client");
}
}
//校验scope是否匹配
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)));
}

//到这里就来到了整个token方法的核心,这一步会生成token,这里spring security设计的很好
//灵活性非常高,通过grantType为每个模式指定不同的TokenGranter,将生成token的操作
//委托给子类
OAuth2AccessToken token = getTokenGranter().grant(tokenRequest.getGrantType(), tokenRequest);
if (token == null) {
throw new UnsupportedGrantTypeException("Unsupported grant type: " + tokenRequest.getGrantType());
}

return getResponse(token);

}

总结一下,token模式,假如你未登录来访问这个接口,也会抛出异常。这个接口的登录是通过配置认证头来实现的,username配置clientId,password配置clientSecret.然后在UsernamePassword*Filter中捕获并注入principal。

这里如果登录无误,就会进入到最重要的方法里,132行的grant中,然后进行生成token操作

然后看一下重要的组件

上一章看完了,那么核心问题转化为了,spring security是怎么生成token的。而生成token的核心方法对应于org.springframework.security.oauth2.provider.TokenGranter这个接口的grant方法.接下来让我们看下这个方法,同时再去了解下spring security的核心组件吧。

TokenGranter

话不多说,直接看代码

首先看一下继承关系:
7
在上面的token模式中,grant方法默认会进入到AbstractTokenGranter的grant方法中,而假如我们需要自定义认证模式,一般做法也是去继承这个抽象的父类,比如MobileCodeTokenGranter

因此直接看下这个自定义的MobileCodeTokenGranter的代码吧:

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
/**
* 自定义grant_type模式-手机号短信验证模式
* @author: yaohw
* @create: 2019-09-29 18:29
**/
public class MobileCodeTokenGranter extends AbstractTokenGranter {

//根据你grant_type参数的不同,这里最后会进入到不同的tokenGranter
//因此如果需要自定义认证模式,一定要修改这个参数
private static final String GRANT_TYPE = "mobile";

private final AuthenticationManager authenticationManager;

public MobileCodeTokenGranter(AuthenticationManager authenticationManager,
AuthorizationServerTokenServices tokenServices, ClientDetailsService clientDetailsService, OAuth2RequestFactory requestFactory) {
this(authenticationManager, tokenServices, clientDetailsService, requestFactory, GRANT_TYPE);
}

@Override
protected OAuth2Authentication getOAuth2Authentication(ClientDetails client, TokenRequest tokenRequest) {

//将参数组装为MobileCodeAuthenticationToken
Map<String, String> parameters = new LinkedHashMap<String, String>(tokenRequest.getRequestParameters());
String mobile = parameters.get("mobile");
String code = parameters.get("code");

Authentication userAuth = new MobileCodeAuthenticationToken(mobile,code);
((AbstractAuthenticationToken) userAuth).setDetails(parameters);
try {
//最后将这个MobileCodeAuthenticationToken交给authenticationManager来认证
userAuth = authenticationManager.authenticate(userAuth);
}
......
}
}

因此认证的核心方法最后交给了AuthenticationManager这个类,而这个类又是通过与其他的组件交互,最后完成认证的:

下面的内容建议直接看该博客:https://www.jianshu.com/p/7b87ec108405
8

简单的说就是,spring security通过不同的grantType,将颁发token的操作委托给了TokenGranter。

而TokenGranter通过将不同的登录参数组装为不同的token,然后将token委托给AuthenticationMnager来处理。

AuthenticationManager根据不同的token类型,通过调用AuthenticationProvider接口的support方法来决定,将这个token委托给哪个provider处理,最后的主要校验逻辑都是由provider来实现的。

spring security oauth 认证服务器

说到底oauth-server其实就是在spring-security的基础上增加了两个入口和一系列过滤器还有上面这些认证组件,两个入口和这些认证组件上面已经分析过了。剩下的核心其实还是spring-security,通过这一系列的过滤器,spring 完成了一系列鉴权与跳转操作,同时为我们预留了很多接口,方便我们自定义

spring security 本身并不复杂,它的核心原理可以用这张图和三局话概括:

  • 整个框架的核心是一个过滤器,这个过滤器名字叫springSecurityFilterChain类型是FilterChainProxy
  • 核心过滤器里面是过滤器链(列表),过滤器链的每个元素都是一组URL对应一组过滤器
  • WebSecurity用来创建FilterChainProxy过滤器,HttpSecurity用来创建过滤器链的每个元素。

9

如果想仔细了解,推荐这个博客,上面三句话也是来自这个博客的: https://blog.csdn.net/zimou5581/article/details/102457672

另外spring security提供了非常多的过滤器,如果想要了解的话,可以看这个博客:https://cloud.tencent.com/developer/article/1551517

内网的auth-server 和 client 的项目结构和改造思路,以及如何配置使用

参见内网文档 以及 代码注释

信息系统中的统一身份认证与授权(一)

本文主要为整个项目中的信息系统认证架构的发展与介绍,并同时给大家科普一些基础的知识点,不涉及比较深奥的技术

主要讲解知识点:

  • 认证、授权、鉴权和权限控制
  • 有状态与无状态
  • CAS单点登录协议
  • Oauth2.0协议

基础概念

1.认证、授权、鉴权和权限控制

本章节主要内容参考的博客链接为: http://www.hyhblog.cn/2018/04/25/user_login_auth_terms/

下面将简要的介绍一下信息安全领域中认证、授权、鉴权和权限控制这四个概念,并对他们之间的关系进行简要的梳理。对于现在的信息系统而言,都是由多个网站和应用组成的,如下图所示:

1

而在一次简单的登录流程中,我们要按顺序完成以下四个步骤: 认证、授权、鉴权和权限控制

1
2
3
4
5
6
7
8
9
10
11
sequenceDiagram
用户->>官网: 想看大数据的页面
官网->>用户: 你是谁?麻烦先登录
用户->>认证中心: 输入用户名密码来登录 (认证)
认证中心->>用户: 给你一个令牌 (授权)
用户->>官网: 带着令牌过来访问大数据页面
官网->>认证中心:校验令牌是真是假 (鉴权)
认证中心->>官网:令牌为真
官网->>认证中心: 这个令牌有没有访问这个页面的权限 (权限控制)
认证中心->>官网: 有权限
官网->>用户: 允许访问,返回界面

接下来将依次对这四个概念进行介绍:

1.1认证

认证是指根据声明者(用户)所特有的识别信息,确认声明者的身份。认证在英文中对应于identification这个单词。

最常见的认证实现方式是通过用户名和密码,但认证方式不限于此。下面都是当前常见到的认证技术,

  • 身份证
  • 用户名和密码
  • 用户手机:手机短信、手机二维码扫描、手势密码
  • 用户的电子邮箱
  • 基于时间序列和用户相关的一次性口令
  • 用户的生物学特征:指纹、语音、眼睛虹膜
  • 用户的大数据识别…

现在认证中心知道这个使用者是用户A了,但是它应该怎么告诉官网呢?

1.2授权

简单来说,授权一般是指获取用户的委派权限。在英文中对应于authorization这个单词。

在信息安全领域,授权是指资源所有者委派执行者,赋予执行者指定范围的资源操作权限,以便执行者代理执行对资源的相关操作。这里面包含有如下四个重要概念

换句人话来说,就是认证中心(资源所有者)颁发一个令牌(包含权限)给用户A正在使用的浏览器(执行者),让这个浏览器(执行者)可以去官网拿数据(对资源执行读取操作)。这个颁发令牌的过程就是授权的一种实现方式。

授权的实现方式非常多也很广泛,我们常见的银行卡、门禁卡、钥匙、公证书,这些都是现实生活中授权的实现方式。其实现方式主要通过一个共信的媒介完成,这个媒介不可被篡改。

在互联网应用开发领域,授权所用到的授信媒介主要包括如下几种,

  • 通过web服务器的session机制,一个访问会话保持着用户的授权信息
  • 通过web浏览器的cookie机制,一个网站的cookie保持着用户的授权信息
  • 颁发授权令牌(token),一个合法有效的令牌中保持着用户的授权信息
    前面两者常见于web开发,需要有浏览器的支持。

1.3鉴权

一句话概括, 鉴定你上一步颁发的令牌的真实性和有效性

1.4权限控制

一句话概括,令牌虽然是真的,但还判断你的令牌有没有权限访问这个接口

2.有状态与无状态

本章节推荐阅读以下博客: https://www.cnblogs.com/shiyajian/p/10672908.html

上一章节这四点,是一个系统的基石。因此接下来所有的认证权限技术方案都是围绕上面四点来设计的,而目前实现统一身份认证和授权的技术手段较多,总体可以归纳为以下两类:

  • 传统的 Cookie + Session 解决方案,有状态会话模式
  • 基于令牌/票据token的解决方案,无状态交互模式

Cookie/session本质上也是一种令牌token,但cookie/session相对于令牌最大的区别的是,服务器端会为每个不同的cookie/session在服务器端保存这个用户对应的信息,服务器拿到session后,会从服务器上取出;而无状态token的用户信息是直接放在token里面的,服务器拿到token后,通过约定的解密方式,将这个token解析出来,来获取这个用户是谁。因此有状态与无状态两种模式最大的区别就在于服务端会不会保存客户端的信息。

这两种模式互有优缺点,不存在哪种技术方案更好的说法。只有最适合的技术,没有最好的技术

无状态 有状态
优点 节省服务器资源,方便水平扩展,适合集群服务器,适合绝大部分设备 服务器可以方便地操控用户的登录状态(踢出,拉黑)
缺点 服务器端难以操控用户的登录状态(踢出登录…) 对服务器资源占用较多,部分设备不支持cookie/session,集群服务器中共享session不方便

目前比较流行的就是互联网APP中大部分采用 JWT 的认证方式,一些企业内部管理系统则大部分采用 cookie-session 的机制,原因可能如下:

1、在互联网APP产品中,尤其以 to C 模式,用户量极大,为了用户体验,一般会将登录信息保留特别长时间,某些APP 只要你不卸载,那么不管几个月之后登录,账户还是处于登录状态。在这种情况下,假如采用 cookie-session 机制,那么你的用户信息保存很多个月,用户量特别大的情况下,会造成大量资源占用和浪费,这种场景采用 JWT 就是相对比较好的方案。

2、企业内部管理系统有以下特点:用户量较少(最多最多不超过10W人),信息安全要求高(及时踢出客户端登录状态,个人浏览器关闭账号退出登录),在这样的场景下占用的内存不会太多,所以基于 cookie-session 这种机制,是比较好的方案,如果企业内部还有其他应用需要集成时候,需要使用 SSO Server 实现。

认证系统的方案发展

2.基于cookie/session机制的cas + shiro方案

项目组成立之初,系统的整体架构还是单体应用的设计方案,因此我们直接沿用了之前项目的登录方案,认证框架采用cas,授权模式为有状态的cookie/session方案,鉴权与权限控制框架为shiro

单点登录的原理可以参考这篇博客: https://www.cnblogs.com/ywlaker/p/6113927.html

CAS的开发可以参考这个博客: https://blog.csdn.net/u010475041/article/details/77886765

而我们这套框架的核心就是CAS,接下来用一句话介绍CAS:
CAS ( Central Authentication Service ) 是 Yale 大学发起的一个企业级的、开源的项目,旨在为 Web 应用系统提供一种可靠的单点登录解决方法。

CAS 单点登录的整体流程可以通过以下一张图来描述清楚:
2

CAS为认证专门设计了几种令牌票据,ST TGC TGT,只要理解了这些票据,整个CAS就不难理解了:

  • TGT

TGT 是CAS-server 为用户签发的登录 ticket,也是用于验证用户登录成功的唯一方式。 TGT 封装了 Cookie 值以及 Cookie 值对应的用户信息,CAS 通过 Cookie 值(TGC)为 key 查询缓存中有无 TGT(TGC:TGT(key:value)),如果有的话就说明用户已经登录了cas-server,无须重复登录。

  • TGC

CAS-server 会将生成的 TGT 放在 session 中,而 TGC 就是这个 session 的唯一标识(sessionId),可以认为是 TGT 的key,为 TGT 就是 TGC 的 value,TGC 以 cookie 的形式保存在浏览器中,每个请求都会尝试携带 TGC。(每个服务都会在 session 和 cookie 中保存对应的 TGT 和 TGC)

  • ST

ST 是当用户访问某一服务时提供的 ticket。用户在访问其他服务时,发现没有 cookie 或 ST ,那么就会重定向到 CAS 服务器获取 ST。然后会携带着 ST 重定向 回来。


因此,我们可以发现,作为一个PC端的单点登录框架,CAS是完美的满足了我们的需求,并且提供了较高的安全性。但是随着移动端和宏天接入了我们的系统,并且我们准备采用微服务架构后,就没那么完美了,因为原来的这套架构会带来以下几个无法避免的问题:

  • 移动端使用cookie/session很不方便
  • 宏天是无状态的微服务架构,采用的是jwt token来认证,与我们现有的有状态cookie/session互不兼容
  • 微服务架构下使用session很不方便,虽然分布式 Session 可以解决这个问题,但因其状态化通信的特性与微服务提倡的API导向无状态通信相互违背,且共享式存储存在安全隐患,因此微服务一般不太采用

其实新版本的cas也是支持无状态token和oauth2.0协议的,只是我们之前使用的cas是4.3版本的,太老了

3.基于无状态token的oauth2.0+jwt+spring-security方案

因此基于以上几个原因,我们将整个项目组的登录架构升级为了以下方案: 认证协议采用oauth2.0,授权模式为无状态的票据token方案,鉴权与权限控制框架为spring-security

oauth2.0推荐参考以下链接: 理解OAuth 2.0OAuth 2.0 的一个简单解释OAuth 2.0 的四种方式

而我们这套框架的核心协议就是Oauth2.0协议,一句话介绍它:
简单说,OAuth 就是一种授权机制。数据的所有者告诉系统,同意授权第三方应用进入系统,获取这些数据。系统从而产生一个短期的进入令牌(token),用来代替密码,供第三方应用使用。

Oauth2.0的核心就只是向第三方应用颁发令牌,但由于现在的互联网有多种场景,因此它为这些场景规定了四种统一的标准流程。 分别是:

  • 授权码(authorization-code)
  • 隐藏式(implicit)
  • 密码式(password)
  • 客户端凭证(client credentials)

因此理解oauth2.0的前提就是理解这四种流程,我在开发这套框架的时候就发生了因为理解有误,导致采用错了流程,最后走了弯路的情况。

授权码模式

授权码(authorization code)方式,指的是第三方应用先申请一个授权码,然后再用该码获取令牌。

这种方式是最常用的流程,大家日常生活中肯定用过,安全性也最高,它适用于那些有后端的 Web 应用。授权码通过前端传送,令牌则是储存在后端,而且所有与资源服务器的通信都在后端完成。这样的前后端分离,可以避免令牌泄漏。

下为具体例子,假如我要登录gitee网站,但是没有账号又不想注册:

  • 第一步, gitee 网站提供一个链接,用户点击后就会跳转到微信登录页面,授权用户数据给gitee网站使用

3

4

下面就是gitee网站跳转微信的一个示意链接:

1
2
3
4
5
6
https://open.weixin.qq.com/connect/qrconnect?
appid=wx63d402790645b7e6&
redirect_uri=https%3A%2F%2Fgitee.com%2Fauth%2Fwechat%2Fcallback&
response_type=code&
scope=snsapi_login
&state=9d90ceb6b3ad0a633a0085541ad024d18bcc96004b20c2a5#wechat_redirect

而标准的oauth2.0跳转链接为:

1
2
3
4
5
https://b.com/oauth/authorize?
response_type=code&
client_id=CLIENT_ID&
redirect_uri=CALLBACK_URL&
scope=read

上面 URL 中,response_type参数表示要求返回授权码(code),client_id参数让 B 知道是谁在请求,redirect_uri参数是 B 接受或拒绝请求后的跳转网址,scope参数表示要求的授权范围(这里是只读)。

  • 第二步,用户跳转后,微信会要求用户登录,然后询问用户是否同意给予 gitee 网站授权。

5

用户表示同意,这时 微信 网站就会跳回redirect_uri参数指定的网址。跳转时,会传回一个授权码,就像下面这样。

1
https://a.com/callback?code=AUTHORIZATION_CODE

上面 URL 中,code参数就是授权码。

  • 第三步,gitee网站拿到授权码以后,就可以在后端,向微信请求令牌。
1
2
3
4
5
6
https://b.com/oauth/token?
client_id=CLIENT_ID&
client_secret=CLIENT_SECRET&
grant_type=authorization_code&
code=AUTHORIZATION_CODE&
redirect_uri=CALLBACK_URL

上面 URL 中,client_id参数和client_secret参数用来让 B确认 A 的身份(client_secret参数是保密的,因此只能在后端发请求),grant_type参数的值是AUTHORIZATION_CODE,表示采用的授权方式是授权码,code参数是上一步拿到的授权码,redirect_uri参数是令牌颁发后的回调网址。

  • 第四步,微信收到请求以后,就会颁发令牌。具体做法是向redirect_uri指定的网址,发送一段 JSON 数据。
1
2
3
4
5
6
7
8
9
{    
"access_token":"ACCESS_TOKEN",
"token_type":"bearer",
"expires_in":2592000,
"refresh_token":"REFRESH_TOKEN",
"scope":"read",
"uid":100101,
"info":{...}
}

这时候gitee的后端就可以拿着这个access_token来访问微信的接口,获取你的信息。整个授权码模式就结束了

通常情况下,第三方应用还会弹出以下界面来让你将微信的个人信息绑定到它的系统中的账号里,这块并不属于oauth2.0的标准流程,是应用自己添加的,这时候第三方应用其实已经拿到了你的微信账号信息。如下图所示:
6

==通过上面的流程,我们不难发现授权码模式主要适用于第三方应用接入你的系统的模式,它的安全性也是非常搞的,敏感信息都是通过后端来调用的,对前端可见的都是不敏感的或者快速失效的信息,并且对于权限粒度的控制也是很好的,就像gitee刚刚申请的那个令牌,就只能获取我的账号信息而已,那个token是没有权限访问其他的信息的==

密码式

如果你高度信任某个应用,用户也可以把用户名和密码,直接告诉该应用。该应用就使用你的密码,调用接口申请令牌,这种方式称为”密码式”(password)。

==这种模式只适用于自己系统内的应用,比如我们现在信息系统内的所有应用 pubs, bms, 移动端都是使用这种模式来获取令牌的。绝对不能给第三方应用使用这种模式==

凭证式和隐藏式

这两种模式其实是很相似的。

隐藏式适合那些纯前端的应用,它们没有后端,必须将令牌储存在前端,因此只能直接将令牌颁发给前端,这种模式是很不安全的。因此,只能用于一些安全要求不高的场景,并且令牌的有效期必须非常短,通常就是会话期间(session)有效,浏览器关掉,令牌就失效了

1
2
3
4
5
https://oauth.b.com/token?
grant_type=password&
username=USERNAME&
password=PASSWORD&
client_id=CLIENT_ID

凭证式适用于没有前端的纯后端应用,也就是后端直接向认证中心申请令牌,而这种方式给出的令牌,是针对第三方应用的,而不是针对用户的,即有可能多个用户共享同一个令牌。

1
2
3
4
https://oauth.b.com/token?
grant_type=client_credentials&
client_id=CLIENT_ID&
client_secret=CLIENT_SECRET

==注意,不管哪一种授权方式,第三方应用申请令牌之前,都必须先到系统备案,说明自己的身份,然后会拿到两个身份识别码:客户端 ID(client ID)和客户端密钥(client secret)。这是为了防止令牌被滥用,没有备案过的第三方应用,是不会拿到令牌的==


授权码模式是oauth2.0四种模式中使用最为广泛也是最安全最复杂的一种,其他三种模式相对来说都偏少一些,只要我们理解了上面四种模式,然后知道它们分别适用于哪些场景,那么oauth2.0对大家来说也就没什么难处的了。

总结

本次分享中主要讲解了以下概念:

  • 认证、授权、鉴权和权限控制
  • 有状态与无状态
  • CAS单点登录协议
  • Oauth2.0协议

当然认证与授权并不只有这些概念,还有cookie/session的实现原理,无状态令牌的实现方式(jwt),以及鉴权框架shiro,spring-security的区别等等这些更为具体的开发知识。但是了解上述的这些概念,已经能对整个系统的架构设计与方案有一个大体的了解了。

工作准备

职位方向

毕业两年主要做的是java后端,前后一共四个项目,交易所官网,信息披露国际化demo, 信息披露国际化, 科交所。涉及的行业主要是金融,国际化。项目的技术侧重点主要是java后端,高可用,工作流。除了工作流,其他的我都算是比较了解,也算是比较了解。技术的宽度还算不错。

因此整体找工作的方向应该是偏向金融,国际化项目(英语),高可用/docker/底层高可用/基础架构,用户体系单点登录, 工作流方向。复习的优先级为优先拓展当前技能的深度,同时发挥自己以上项目经验的优势,拓宽各个方向的深度。

后端常见要求

看了一下常见的后端岗位,基本要求摘要如下:

  1. Java基础扎实,理解io、多线程、集合等基础框架,对JVM原理有一定的了解;熟悉面向对象设计开发;熟悉单元测试代码的编写;
  2. 熟悉linux操作系统,对各种开源框架如Spring、MyBatis,Memcache、Redis、消息队列等有深入的了解;对缓存、消息队列、异步任务、负载均衡、分库分表等后端技术有充分了解和实践
  3. 熟悉数据库原理和技术,熟练掌握 MySQL、MongoDB数据库,精通关系型数据库及SQL优化
  4. 熟悉分布式系统的设计和应用,熟悉分布式、缓存、消息、搜索\推荐等机制;能对分布式常用技术进行合理应用,解决问题;有大型分布式、高负载、高可用性系统设计和稳定性经验优先;熟悉分布式系统原理和设计,熟悉Redis 、Activemq 、Zookeeper等开源项目,能合理进行技术选型,善于解决问题;熟悉互联网应用架构,在高可用系统设计,开发和调优方面有实际经验;
  5. 具有高并发大数据量系统的开发和维护经验,高业务复杂度相关系统的架构设计;有高并发服务设计和实现经验优先;;对大数据技术,如Spark、Hadoop、Flink、ELK等有使用经验者优先;
  6. 我们希望你对互联网或J2EE应用开发的最新潮流有关注,喜欢去看及尝试最新的技术,追求编写优雅的代码,从技术趋势和思路上能影响技术团队;喜欢钻研新技术,热衷于分享。

总结提炼一下分为:

  • 语言基础
  • 框架原理
  • 中间件原理
  • 数据库原理
  • 分布式
  • 高并发
  • 高可用,负载均衡,架构设计
  • 大数据,区块链,k8s等新技术

工作项目经验的侧重点(都有项目经验,可以作为特点深入研究发挥一下):

  • 金融
  • 国际化项目(英语)
  • 高可用/docker/底层高可用/基础架构
  • 用户体系单点登录
  • 工作流方向

基础软工复习大纲

1.计算机网络

2.TCP/IP详解

3.数据结构与算法

4.数据库系统概念

5.操作系统

网络及分布式计算

计算机组织与结构体系

后端开发基础知识复习大纲

  • linux
  • nginx等底层应用
  • 中间件原理
  • 数据库原理
  • 分布式
  • 高并发
  • 高可用,负载均衡,架构设计

JAVA领域复习大纲

  • 语言基础
  • 框架原理

其他领域学习路线

  • 大数据
  • 区块链
  • k8s等新技术

写在2020年的话

不知不觉2020已经过去大半了,我这个拖延症重度患者在拖延了一年多以后,终于也算是用另一种方式完成了个人网站的一部分——博客网站,虽然用的是现有的轮子哈哈哈。刚开始有这个写博客想法的时候,其实是想从零自己造一个博客轮子,但是实在是高估了自己的决心,然后望而却步一拖再拖,而今终于踩在前人的肩膀上算是完成了一个小目标。

2020

过往已过

​ 写这篇日记的时候正是20年6月底,从18年7月毕业南下深圳入职以来,一晃已经过去两年了。身为一个入行两年的菜鸟,越发感觉到了自己的不足与微小,中间也曾经一度动摇过,我到底适不适合在这行继续深耕下去,但是想想自己这两年的所作所为,其实并不是天赋不够,而是努力程度低到了都不用拼天赋的可笑地步。虽然也不觉得自己能成为这一行里拔尖的那些人,但觉得如果能拿出当年高考的拼劲儿,能在这行混个不错,吃饱饭还是可以的。

​ 我不是一个喜欢总结过去经历的人,过去的就过去了,活在过去很累也没有必要,虽然晚上睡觉之前,那些过往的画面 比如种种不堪 或是快乐时光 偶尔也会如幻灯片一样在眼前略过,但是也只是略过而已。虽然活在过去确实没必要,但总结过往的缺点还是很有必要的。回首过往两年的经历,我觉得自己最大的缺点一个是没有勇气,一个是很怠惰,简单的说其实就是又怂又懒。懒惰贪图安逸的天性在最近轮番被刺激后,改进了很多,目前的成果就是这个博客网站以及这篇博客,希望以后能产出更多高质量的博客,向懒惰的自己说不;怂的话,很难解决了,根源在于没有自信,要解决说难也难,说不难也不难,那就是变得强大(物质,精神)

未来可期

过往已过,现在是2020/6/28 今年剩下的时间也不多,三个学习上的小目标:

  • 努力准备争取在今年找到一份心仪的工作

  • 英语提升到听说读写 可以正常沟通与阅读文档的水平

  • 没事学学金融与交易,弄点小钱玩玩金融产品

(这点就不用担心了,身为一个玩比特币交过不少学费的人,控制仓位止损止盈不频繁交易是基本原则还是知道的哈哈哈哈)

现在

​ 最近这段时间其实是很忙的,但就我观察而言,工作上的忙碌与频繁加班只不过是因为项目组的管理极其混乱造成的,项目组前期工作的准备不充分,多个领导的干预,需求与产品地位的低下与话语权缺失,导致整个开发组一直在跟着需求的反复地变更进行反复地修改,真是有点醉,很讨厌这种极其频繁的需求变更引起的加班与这种乱七八糟的工作节奏。我得学会从这种节奏中抽出时间来,既然打算要走了,那就得以自己的生活为核心与重点。毕竟握不住的沙,那就扬了它,不太喜欢的工作,那就拜拜了您嘞~

鲤鱼王,你什么时候才能变成暴鲤龙呢

resume

雷振

个人信息

  • 性 别:男                       年 龄:25
  • 手 机:13006189736                   邮 箱:13006189736@163.com
  • 专 业:软件工程                   岗 位:后台开发工程师

工作及教育经历

  • 武汉大学             2014.9~2018.7      计算机学院-软件工程专业-本科
  • 深圳证券通信有限公司     2018.7~至今       交易结算事业部-后台开发工程师

专业技能

  • 熟悉计算机网络和操作系统等基础知识,有linux(centos/redhat)维护部署排查问题以及shell脚本经验,通过了红帽RHCE认证;
  • 掌握了Java常见的开发知识,理解多线程、集合等基础框架,对JVM原理和调优有一定的了解;熟悉面向对象设计开发;了解数据结构与算法,熟练使用设计模式;
  • 熟悉数据库原理和技术,熟练掌握 MySQL数据库和redis,对底层原理及SQL优化有一定的了解和经验;
  • 熟悉spring/springboot,mybatis框架的日常使用和开发,能熟练使用nginx等中间件,对springboot的底层原理和springcloud的使用有一定的认识和了解;
  • 有一定的架构经验,参与过项目中的架构设计,在高可用负载均衡系统设计,开发方面有实际经验;
  • 掌握CAS单点登录,jwt, oauth2.0/spring security等安全认证相关技术,对常见用户体系,权限角色模型有一定的认识;

项目经历

  1. 深圳证券交易所 - 官方网站招聘模块  (2018.08- 2018.9)
    • 项目描述: 由于旧版官网技术陈旧,因此需要在深圳证券交易所新版官网的基础上重新开发招聘子模块。
    • 责任描述:
      • 熟悉所内的开发框架,并修复项目中的部分缺陷
      • 参与项目的上线部署流程,熟悉所内网络环境以及各种工具
      • 参与该模块部分业务接口的开发工作,对提交数据进行格式校验以及业务校验
  2. 深圳证券交易所 - 信息披露国际化演示demo  (2018.10- 2019.01)
    • 项目描述: 为了响应国家一带一路政策,并且将我国证券技术系统输出至海外,我们团队负责将所内的复杂的信息披露系统简单化,国际化。做出一个可以用于演示以及宣传的信息披露demo系统,以方便商务会谈使用。
    • 责任描述:
      • 参与了信息披露展示系统的开发
      • 参与了业务流程管理演示系统的开发
      • 参与了文件传输系统的改版
      • 参与技术文档以及使用说明书的编写
      • 参与项目的日常部署以及缺陷修复
      • 参与了后端工作流框架和部分流程处理框架的搭建,同时开发了多条流程。加深了对springboot, sso,工作流以及信息披露业务的理解
  3. 深圳证券交易所 - 信息披露国际化系统  (2019.02-2020.06)
    • 项目描述: 由于国内外的信息披露规则差异过大,国内的系统并不能完全照搬至国外,我们团队决定完全推翻上一个演示demo的设计思路,重新设计一个信息披露通用国际化产品。并且为了避免不必要的知识产权纠纷,将部分框架更换为拥有较为宽松的开源协议的技术框架。
    • 责任描述:
      • 参与了前期信息披露通用国际化产品的构思与设想
      • 负责排查项目中的开源协议,并对部分开源组件进行替换与修改
      • 深度参与信息披露国际化产品的需求讨论与架构设计,并进行技术验证
      • 参与工作流框架的选型以及poc验证
      • 负责搭建信息披露国际化产品的开发测试等各种环境,并负责日常的上线部署与维护
      • 协助开发经理进行信息披露国际化产品的基础框架搭建
      • 负责信息披露国际化产品SSO系统的设计与开发:引入ldap并与现有系统的用户管理模块对接,支持多种方式多种平台登录
      • 负责部分业务流程的开发,以及对所实现模块进行代码评审和优化
      • 参与了信息披露展示系统,业务流程管理系统,配置管理系统的业务开发
      • 负责linux环境下自动部署备份脚本的编写
      • 负责信息披露国际化产品的部署文档,运维文档,开发文档的编写以及部分文档的初稿翻译
      • 参与了外宾的接待以及后续的技术支持与培训工作
  4. 深圳证券交易所 - 科技与知识产权交易中心系统  (2020.06- 至今)
    • 项目描述: 为了配合广东省的建设全国知识产权交易中心的战略实施重点,大力推进知识产权质押融资、专利保险、产业基金等多项工作,同时推动建立深圳市的知识产权交易机制,实现交易双方快速匹配,降低交易成本。因此正在建设该科技与知识产权交易中心的核心系统。
    • 责任描述:
      • 配合开发经理完成核心系统的项目架构设计与搭建
      • 负责项目的日常部署,环境维护,上线,线上问题处理以及部分技术文档编写
      • 负责项目的用户体系的设计与开发
      • 负责项目的安全漏洞修复
      • 负责项目的部分业务模块,流程,以及专区的开发
      • 负责单点登录系统与移动端登陆,扫码登陆等功能的开发,以及与vnext,区块链,人脸识别等外部系统集成并对接
      • 参与代码评审与数据库评审,并负责对部分模块和框架进行优化
      • 负责官网系统与业务系统部分业务模块的整体设计与开发
      • 负责新版认证系统开发,引入spring security 和oauth2.0
      • 学习网易轻舟微服务框架并接收培训,尝试将项目整体迁移改造至网易轻舟微服务平台
      • 学习容器化技术,探索将项目容器化并采用k8s进行管理的可能性

获奖经历

  • 研发部门季度之星两次
  • 深圳证券交易所技术口2020进步最快新人奖

个人账号

其他信息

  • 喜欢钻研技术,目前正在学习Go和k8s
  • 当过小组长,有一定的项目管理和任务安排经验
  • 喜欢徒步,摄影与旅游