认证与授权
认证
HTTP 认证
如果需要认证,服务端会返回这样的信息:
WWW-Authenticate: <认证方案> realm=<保护区域的描述信息>
Proxy-Authenticate: <认证方案> realm=<保护区域的描述信息>
客户端接收到后,需要遵循服务的指定的认证方案:
Authorization: <认证方案> <凭证内容>
Proxy-Authorization: <认证方案> <凭证内容>
服务端进行认证,根据成功与否返回200或者403
对于凭证内容,默认是Base64编码,但还有其他的一些认证方案:
- Digest
- Bearer
- HOBA
表单认证
对于表单认证 并没有一个通用的标准 应该这些内容必须放到应用层面解决
WebAuthn:新的认证标准
Java的实现
- JACC
- JASPIC
- EE Security
但实际上 活跃在Java安全领域的是两个私有标准 Shiro 和 Spring Security
SSO
- 单点登录
大型互联网公司中,公司旗下可能会有多个子系统,每个登陆实现统一管理多个账户信息统一管理 SSO单点登陆认证授权系统
CAS
CAS 是 Yale 大学发起的一个开源项目,旨在为 Web 应用系统提供一种可靠的单点登录方法
sequenceDiagram
浏览器 ->> CAS客户端: 访问
CAS客户端 ->> CAS服务器: 重定向并携带service
CAS服务器 ->> 浏览器: 跳转,进行用户认证
浏览器 ->> CAS服务器: 认证完成
CAS服务器 ->> CAS客户端: 重定向并携带ticket
CAS客户端 ->> CAS服务器: 验证ticket
CAS服务器 ->> CAS客户端: 返回用户信息
CAS客户端 ->> 浏览器: 提供服务
- 访问服务:SSO客户端发送请求访问应用系统提供的服务资源。
- 定向认证:SSO客户端会重定向用户请求到SSO服务器。
- 用户认证:用户身份认证。
- 发放票据:SSO服务器会产生一个随机的Service Ticket。
- 验证票据:SSO服务器验证票据Service Ticket的合法性,验证通过后,允许客户端访问服务。
- 传输用户信息:SSO服务器验证票据通过后,传输用户认证结果信息给客户端。
术语:
- Ticket Granting ticket (TGT) :可以认为是CAS Server根据用户名密码生成的一张票,存在Server端
- Ticket-granting cookie (TGC) :其实就是一个Cookie,存放用户身份信息,由Server发给Client端
- Service ticket (ST) :由TGT生成的一次性票据,用于验证,只能用一次。相当于Server发给Client一张票,然后Client拿着这个票再来找Server验证,看看是不是Server签发的
OIDC
一个基于授权码流程的 OIDC 协议流程,跟 OAuth 2.0 中的授权码许可的流程几乎完全一致,唯一的区别就是多返回了一个 ID 令牌,用访问令牌获取 ID 令牌之外的信息
这个 ID 令牌的内容如下:
- iss:令牌的颁发者,其值就是身份认证服务(OP)的 URL
- sub:令牌的主题,其值是一个能够代表最终用户(EU)的全局唯一标识符
- aud:令牌的目标受众,其值是三方软件(RP)的 app_id
- exp:令牌的到期时间戳,所有的 ID 令牌都会有一个过期时间
- iat:颁发令牌的时间戳。
授权
系统如何控制一个用户该看到哪些数据、能操作哪些功能
DAC
Discretionary Access Control,自主访问控制,让客体的所有者来定义访问控制规则
Linux 中采用的就是 DAC,用户可以控制自己的文件能够被谁访问
Role-BAC
stateDiagram-v2
direction LR
用户(user) --> 角色(role): 隶属
角色(role) --> 许可(permission): 拥有
许可(permission) --> 资源(resource): 操作
简化了配置操作 并且满足了最小特权原则
- RBAC-1模型可以描述角色继承关系
- RBAC02模型可以描述角色互斥
Rule-BAC
针对请求本身制定的访问控制策略
适合在复杂场景下提供访问控制保护,因此,rule-BAC 相关的设备和技术在安全中最为常见。一个典型的例子就是防火墙
MAC
Mandatory Access Control,强制访问控制
为了保证机密性,MAC 不允许低级别的主体读取高级别的客体、不允许高级别的主体写入低级别的客体;为了保证完整性,MAC 不允许高级别的主体读取低级别的客体,不允许低级别的主体写入高级别的客体
OAuth2
OAuth 2.0 授权的核心就是颁发访问令牌、使用访问令牌
sequenceDiagram
三方应用 ->> 资源所有者: 要求用户给予授权
资源所有者 -->> 三方应用: 同意给予该应用授权
三方应用 ->> 授权服务器: 我有用户授权,申请访问令牌
授权服务器 -->> 三方应用: 同意发放访问令牌
三方应用 ->> 资源服务器: 我有访问令牌,申请开放资源
资源服务器 ->> 三方应用: 同意开放资源
前置概念:
- appId:三方应用ID
- appSecret:三方应用密钥
- 授权码:用来获取accessToken
- scope:所需的数据的范围
- AccessToken:调用接口权限访问令牌
- 回调地址:授权成功,重定向地址
- openid:开放平台生产唯一的用户id
授权码想要换取令牌还得再加上appId与appKey
授权服务工作流程
- 令牌的生成必须符合三个原则:唯一性、不连续性、不可猜性
- OAuth 2.0 规范建议授权码 code 值有效期为 10 分钟,并且一个授权码 code 只能被使用一次
- 一个访问令牌 access_token 表示某一个用户给某一个第三方软件进行授权
- 授权要有授权范围,不能让第三方软件获得比注册时权限范围还大的授权,也不能获得超出了用户授权的权限范围,始终确保最小权限安全原则
sequenceDiagram
opt 线下
三方应用 ->> 授权服务: 提交回调地址,scope进行注册
授权服务 -->> 三方应用: 发送appId与appSecret
end
opt 授权码获取
用户 ->> 三方应用: 使用
三方应用 ->> 授权服务: 携带id、secret、回调地址、scope
授权服务 -->> 授权服务: 验证基本信息(id、secret、回调地址对得上),scope范围是否合规
授权服务 -->> 用户: 生成授权页面,显示三方应用所要的数据
用户 ->> 授权服务: 提交同意授权的scope
授权服务 -->> 授权服务: 校验应用索取的scope是否超出用户授权的scope,并且把授权码跟范围绑定
授权服务 -->> 三方应用: 重定向并给予授权码
end
opt 访问令牌获取
三方应用 ->> 授权服务: 携带id、secret、授权码
授权服务 -->> 授权服务: 校验id、secret,将scope与访问令牌做绑定,删除当前授权码
授权服务 -->> 授权服务: 生成刷新令牌
授权服务 -->> 三方应用: 返回访问令牌与刷新令牌
end
opt 访问令牌刷新
三方应用 ->> 授权服务: id、secrect、刷新令牌
授权服务 ->> 授权服务: 基本信息校验
授权服务 ->> 授权服务: 重新生成访问令牌、刷新令牌,并失效掉之前的令牌与刷新令牌
授权服务 -->> 三方应用: 返回访问令牌与刷新令牌
end
授权码模式
sequenceDiagram
资源所有者 ->> 操作代理: 通过操作代理访问应用
操作代理 ->> 第三方应用: 遇到需要使用的资源
第三方应用 ->> 授权服务器: 转向授权服务器的授权页面
资源所有者 ->>+ 授权服务器: 认证身份,同意授权
授权服务器 -->>- 操作代理: 返回第三方应用的回调地址,附带授权码
操作代理 ->> 第三方应用: 转向回调地址
第三方应用 ->>+ 授权服务器: 将授权码发回给授权服务器,换取访问令牌
授权服务器 -->>- 第三方应用: 给予访问令牌、刷新令牌
opt 资源访问过程
第三方应用 ->>+ 资源服务器: 提供访问令牌
资源服务器 -->>- 第三方应用: 提供返回资源
第三方应用 -->> 资源所有者: 返回对资源的处理给用户
end
授权码一般是一次性,使用授权码再去获取令牌的原因在于为了避免直接将令牌暴露给操作代理带来的不安全性
这种模式考虑到了许多种情况,比如授权码泄露被人冒充、三方应用被人冒充等。 但是是在假设第三方应用有自己的服务器的基础上 而且授权过程也过分繁琐
隐式授权
sequenceDiagram
资源所有者 ->> 操作代理: 通过操作代理访问应用
操作代理 ->> 第三方应用: 遇到需要使用的资源
第三方应用 ->> 授权服务器: 转向授权服务器的授权页面
资源所有者 ->> 授权服务器: 认证身份,同意授权
授权服务器 -->> 操作代理: 返回第三方应用的回调地址,通过Fragment附带访问令牌
操作代理 ->> 第三方应用: 转向回调地址,通过脚本提取出Fragment中的令牌
Fragment是不会跟随请求被发送到服务端的,只能在客户端通过Script脚本来读取。所以隐式授权巧妙地利用这个特性,尽最大努力地避免了令牌从操作代理到第三方服务之间的链路存在被攻击而泄漏出去的可能性
密码模式
sequenceDiagram
资源所有者 ->> 三方应用: 提供密码凭证
三方应用 ->> 授权服务器: 发送用户的密码凭证
授权服务器 -->> 三方应用: 发放访问令牌和刷新令牌
这种情况下需要把密码提供给第三方 要求第三方必须十分可信
客户端模式
sequenceDiagram
应用 ->> 授权服务器: 申请授权
授权服务器 -->> 应用: 发放访问令牌
在获取一种不属于任何一个第三方用户的数据时,并不需要用户参与,此时便可以使用客户端凭据许可类型
PKCE协议
- 这种协议适用于没有服务器的移动端程序,是一种在失去 app_secret 保护的时候,防止授权码失窃的解决方案
sequenceDiagram
三方应用 -->> 三方应用: 创建验证码,创建挑战码 = BASE64(SHA256(ASCII(验证码)))
opt 获取授权码
三方应用 ->> 授权服务: 发送挑战码以及挑战吗的加密方式
授权服务 -->> 授权服务: 保存挑战码及授权码
授权服务 -->> 三方应用: 返回授权码
end
opt 获取访问令牌
三方应用 ->> 授权服务: 发送验证码+授权码
授权服务 -->> 授权服务: 校验验证码加密后是否跟挑战码、授权码三码合一
授权服务 -->> 三方应用: 返回访问令牌
end
凭证
令牌
通过app id与 app secret 获取一个临时token,此token具有操作的权限
Cookie-Session
通过在响应头设置这么样的一项:
Set-Cookie: id=cxk; Expires=Wed, 21 Feb 2020 07:28:00 GMT; Secure; HttpOnly
后客户端每次请求都会将这个Cookie带上到请求头
GET /index.html HTTP/2.0
Host: www.baidu.com
Cookie: id=cxk
但系统可以将这个Cookie以一个key看待,在服务端开辟一块内存,形成一个KV对,这就是Session
但 Cookie会有跨域问题, Sesssion 在集群环境下又会有问题
JWT
JSON Web Token(JWT)是一个开放的行业标准(RFC 7519),它定义了一种简介的、自包含的协议格式,用于 在通信双方传递json对象,传递的信息经过数字签名可以被验证和信任。JWT可以使用HMAC算法或使用RSA的公 钥/私钥对来签名,防止被篡改
sequenceDiagram
客户端 ->> 认证服务: 用户认证
认证服务 ->> 客户端: 返回JWT令牌
客户端 ->> 资源服务: 携带令牌访问资源
资源服务 ->> 资源服务: 校验令牌
资源服务 ->> 客户端: 返回资源
无状态,既是优点 也是缺点 虽然可以进行无状态服务节点水平扩展 但在某些业务场景下 实现某些功能还是优点困难
为了解决无状态带来难以让令牌失效的问题,有一些办法:
- 引入统一秘钥管理,每个用户都有自己的秘钥,一旦想要失效令牌,就可以通过重新生成秘钥的方式来进行
- 只考虑用户修改密码失效令牌的情况,则可以通过直接用用户的密码当秘钥
- 缺点:令牌长度较长,这就意味着传输会有问题,某些服务器对Header是有限制的
组成
- 头部
{"typ":"JWT","alg":"HS256"} // 经过base64加密后:eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9
- 载荷
- 标准中注册的声明
- 公共的声明
- iss(Issuer):签发人。
- exp(Expiration Time):令牌过期时间。
- sub(Subject):主题。
- aud (Audience):令牌受众。
- nbf (Not Before):令牌生效时间。
- iat (Issued At):令牌签发时间。
- jti (JWT ID):令牌编号
- 私有的声明
{"sub":"1234567890","name":"John Doe","admin":true} // eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9
- 签证
header (base64后的)
payload (base64后的)
使用secret对header以及payload进行一个签名
secret是保存在服务器端的,jwt的签发生成也是在服务器端的,secret就是用 来进行jwt的签发和jwt的验证,所以,它就是你服务端的私钥,在任何场景都不应该流露出去
JJWT
- 依赖
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
- 构建
JwtBuilder jwtBuilder = Jwts.builder()
.setId("jntm")
.setSubject("cxk")
.setIssuedAt(new Date())
.signWith(SignatureAlgorithm.HS256,"1234")
.claim("role","admin")
.setExpiration(new Date(System.currentTimeMillis()+300));
System.out.println(jwtBuilder.compact());
- 解析
Claims body = Jwts.parser().setSigningKey("1234")
.parseClaimsJws("jwt")
.getBody();
System.out.println(body.getId()+"|"+body.getSubject()+"|"+body.getIssuedAt());
OpenID
保密
保密是有成本的,追求越高的安全等级,就要付出越多的工作量与算力消耗
客户端加密
客户端加密并非是为了传输安全 传输安全应该由诸如HTTPS等的机制来进行保障 更多地 客户端加密是为了避免明文传输到服务端后造成的安全问题
密码加密与存储
加密
- 客户端对自己的密码取摘要:
const passwd = 123456
const client_hash = MD5(passwd)
- 得到摘要后进行加盐:
client_hash = MD5(client_hash + salt)
为了应对彩虹表类的暴力破解,摘要函数可以使用慢哈希函数 也就是执行时间可以调节的函数(比如Bcrypt)
- 为了防止服务端被脱库,服务端再使用一个盐:
String salt = randomSalt();
String serverHash = SHA256(client_hash + salt)
addToDB(serverHash, salt)
验证
- 客户端加密还是同上,进行加盐哈希
client_hash = MD5(MD5(passwd) + salt)
- 服务端接收到client_hash 后,对其加盐哈希,判断是否与存储的一致:
compare(server_hash, SHA256(client_hash + server_salt))
Bcrypt
bcrypt会使用一个加盐的流程以防御彩虹表攻击,同时bcrypt还是适应性函数,它可以借由增加迭代之次数来抵御日益增进的电脑运算能力透过暴力法破解
- 每次加密都会产生一个随机的salt跟密文进行哈希并拼接到最终的结果中
- 验证时需要salt加密文同时进行运算才能进行验证
开放平台设计
在互联网时代,把网站的服务封装成一系列计算机易识别的数据接口开放出去,供第三方开发者使用,这种行为就叫做Open API,提供开放API的平台本身就被称为开放平台
- 使用加签名方式,防止篡改数据
- 使用HTTPS加密传输
- 搭建OAuth2.0认证授权
- 使用令牌方式
- 搭建网关实现黑名单和白名单
参数传递安全
后端服务器传递参数,返回token给前端,前端通过token请求另外一台服务器
接口版本控制
- RPC接口:代码发包
- HTTP接口:Path、Header
使用网关分发不同版本请求
SpringCloudOAuth2
授权服务端
- 依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>
- 配置
@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
// accessToken有效期
private int accessTokenValiditySeconds = 7200; // 两小时
// 添加商户信息
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
// withClient appid
clients.inMemory().withClient("client_1")
.redirectUris("http://www.baidu.com")
.secret(passwordEncoder().encode("123456"))
.authorizedGrantTypes("password","client_credentials","refresh_token","authorization_code").scopes("all").accessTokenValiditySeconds(accessTokenValiditySeconds);
}
// 设置token类型
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
endpoints.authenticationManager(authenticationManager()).allowedTokenEndpointRequestMethods(HttpMethod.GET,
HttpMethod.POST);
}
@Override
public void configure(AuthorizationServerSecurityConfigurer oauthServer) {
// 允许表单认证
oauthServer.allowFormAuthenticationForClients();
// 允许check_token访问
oauthServer.checkTokenAccess("permitAll()");
}
@Bean
AuthenticationManager authenticationManager() {
return authentication -> daoAuhthenticationProvider().authenticate(authentication);
}
@Bean
public AuthenticationProvider daoAuhthenticationProvider() {
DaoAuthenticationProvider daoAuthenticationProvider = new DaoAuthenticationProvider();
daoAuthenticationProvider.setUserDetailsService(userDetailsService());
daoAuthenticationProvider.setHideUserNotFoundExceptions(false);
daoAuthenticationProvider.setPasswordEncoder(passwordEncoder());
return daoAuthenticationProvider;
}
// 设置添加用户信息,正常应该从数据库中读取
@Bean
UserDetailsService userDetailsService() {
InMemoryUserDetailsManager userDetailsService = new InMemoryUserDetailsManager();
userDetailsService.createUser(User.withUsername("user_1").password(passwordEncoder().encode("123456"))
.authorities("ROLE_USER").build());
userDetailsService.createUser(User.withUsername("user_2").password(passwordEncoder().encode("123456"))
.authorities("ROLE_USER").build());
return userDetailsService;
}
@Bean
PasswordEncoder passwordEncoder() {
// 加密方式
return new BCryptPasswordEncoder();
}
}
@Component
public class SecurityConfig extends WebSecurityConfigurerAdapter {
// 授权中心管理器
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
// 拦截所有请求,使用httpBasic方式登陆
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().antMatchers("/**").fullyAuthenticated().and().httpBasic();
}
}
根据code获取获取access_token
资源端
- 依赖同授权服务端
- 配置
security:
oauth2:
resource:
####从认证授权中心上验证token
tokenInfoUri: http://localhost:9000/oauth/check_token
preferTokenInfo: true
client:
accessTokenUri: http://localhost:9000/oauth/token
userAuthorizationUri: http://localhost:9000/oauth/authorize
###appid
clientId: client_1
###appSecret
clientSecret: 123456
@Configuration
@EnableResourceServer
public class ResourceServerConfiguration extends ResourceServerConfigurerAdapter {
@Override
public void configure(HttpSecurity http) throws Exception {
// 对 api 请求进行拦截
http.authorizeRequests().antMatchers("/api").authenticated();
}
}
@EnableOAuth2Sso