认证与授权

认证

HTTP 认证

如果需要认证,服务端会返回这样的信息:

WWW-Authenticate: <认证方案> realm=<保护区域的描述信息>
Proxy-Authenticate: <认证方案> realm=<保护区域的描述信息>

客户端接收到后,需要遵循服务的指定的认证方案:

Authorization: <认证方案> <凭证内容>
Proxy-Authorization: <认证方案> <凭证内容>

服务端进行认证,根据成功与否返回200或者403

对于凭证内容,默认是Base64编码,但还有其他的一些认证方案:

表单认证

对于表单认证 并没有一个通用的标准 应该这些内容必须放到应用层面解决

WebAuthn:新的认证标准

Java的实现

但实际上 活跃在Java安全领域的是两个私有标准 Shiro 和 Spring Security

SSO

大型互联网公司中,公司旗下可能会有多个子系统,每个登陆实现统一管理多个账户信息统一管理 SSO单点登陆认证授权系统

典型SSO架构

CAS

CAS 是 Yale 大学发起的一个开源项目,旨在为 Web 应用系统提供一种可靠的单点登录方法

体系结构

sequenceDiagram
    浏览器 ->> CAS客户端: 访问
    CAS客户端 ->> CAS服务器: 重定向并携带service
    CAS服务器 ->> 浏览器: 跳转,进行用户认证
    浏览器 ->> CAS服务器: 认证完成
    CAS服务器 ->> CAS客户端: 重定向并携带ticket
    CAS客户端 ->> CAS服务器: 验证ticket
    CAS服务器 ->> CAS客户端: 返回用户信息
    CAS客户端 ->> 浏览器: 提供服务
  1. 访问服务:SSO客户端发送请求访问应用系统提供的服务资源。
  2. 定向认证:SSO客户端会重定向用户请求到SSO服务器。
  3. 用户认证:用户身份认证。
  4. 发放票据:SSO服务器会产生一个随机的Service Ticket。
  5. 验证票据:SSO服务器验证票据Service Ticket的合法性,验证通过后,允许客户端访问服务。
  6. 传输用户信息:SSO服务器验证票据通过后,传输用户认证结果信息给客户端。

术语:

OIDC

一个基于授权码流程的 OIDC 协议流程,跟 OAuth 2.0 中的授权码许可的流程几乎完全一致,唯一的区别就是多返回了一个 ID 令牌,用访问令牌获取 ID 令牌之外的信息

这个 ID 令牌的内容如下:

授权

系统如何控制一个用户该看到哪些数据、能操作哪些功能

DAC

Discretionary Access Control,自主访问控制,让客体的所有者来定义访问控制规则

Linux 中采用的就是 DAC,用户可以控制自己的文件能够被谁访问

Role-BAC

202031885639

stateDiagram-v2
  direction LR
  用户(user) --> 角色(role): 隶属
  角色(role) --> 许可(permission): 拥有
  许可(permission) --> 资源(resource): 操作

简化了配置操作 并且满足了最小特权原则

Rule-BAC

针对请求本身制定的访问控制策略

适合在复杂场景下提供访问控制保护,因此,rule-BAC 相关的设备和技术在安全中最为常见。一个典型的例子就是防火墙

MAC

Mandatory Access Control,强制访问控制

为了保证机密性,MAC 不允许低级别的主体读取高级别的客体、不允许高级别的主体写入低级别的客体;为了保证完整性,MAC 不允许高级别的主体读取低级别的客体,不允许低级别的主体写入高级别的客体

OAuth2

OAuth 2.0 授权的核心就是颁发访问令牌、使用访问令牌

sequenceDiagram
    三方应用 ->> 资源所有者: 要求用户给予授权
    资源所有者 -->> 三方应用: 同意给予该应用授权
    三方应用 ->> 授权服务器: 我有用户授权,申请访问令牌
    授权服务器 -->> 三方应用: 同意发放访问令牌
    三方应用 ->> 资源服务器: 我有访问令牌,申请开放资源
    资源服务器 ->> 三方应用: 同意开放资源

前置概念:

授权码想要换取令牌还得再加上appId与appKey

授权服务工作流程

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协议

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令牌
    客户端 ->> 资源服务: 携带令牌访问资源
    资源服务 ->> 资源服务: 校验令牌
    资源服务 ->> 客户端: 返回资源

无状态,既是优点 也是缺点 虽然可以进行无状态服务节点水平扩展 但在某些业务场景下 实现某些功能还是优点困难

为了解决无状态带来难以让令牌失效的问题,有一些办法:

  1. 引入统一秘钥管理,每个用户都有自己的秘钥,一旦想要失效令牌,就可以通过重新生成秘钥的方式来进行
  2. 只考虑用户修改密码失效令牌的情况,则可以通过直接用用户的密码当秘钥

组成

{"typ":"JWT","alg":"HS256"} // 经过base64加密后:eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9
{"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等的机制来进行保障 更多地 客户端加密是为了避免明文传输到服务端后造成的安全问题

密码加密与存储

加密

  1. 客户端对自己的密码取摘要:
const passwd = 123456
const client_hash = MD5(passwd)
  1. 得到摘要后进行加盐:
client_hash = MD5(client_hash + salt)

为了应对彩虹表类的暴力破解,摘要函数可以使用慢哈希函数 也就是执行时间可以调节的函数(比如Bcrypt)

  1. 为了防止服务端被脱库,服务端再使用一个盐:
String salt = randomSalt();
String serverHash = SHA256(client_hash + salt)
addToDB(serverHash, salt)

验证

  1. 客户端加密还是同上,进行加盐哈希
client_hash = MD5(MD5(passwd) + salt)
  1. 服务端接收到client_hash 后,对其加盐哈希,判断是否与存储的一致:
compare(server_hash, SHA256(client_hash + server_salt))

Bcrypt

bcrypt会使用一个加盐的流程以防御彩虹表攻击,同时bcrypt还是适应性函数,它可以借由增加迭代之次数来抵御日益增进的电脑运算能力透过暴力法破解

Bcrypt组成

开放平台设计

在互联网时代,把网站的服务封装成一系列计算机易识别的数据接口开放出去,供第三方开发者使用,这种行为就叫做Open API,提供开放API的平台本身就被称为开放平台

参数传递安全

后端服务器传递参数,返回token给前端,前端通过token请求另外一台服务器

接口版本控制

使用网关分发不同版本请求

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();
    }
}

通过 http://localhost:9000/oauth/authorize?response_type=code&client_id=client_1&redirect_uri=http://www.baidu.com&scope=all获取code

根据code获取获取access_token

http://localhost:9000/oauth/token?grant_type=authorization_code&code=zCn8Gl&redirect_uri=http://www.baidu.com&scope=all&password=123456

资源端

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