User authentication and authorization services micro Zatan

[TOC]


There VS stateless status

Almost most of the applications need to implement authentication and authorization, such as user account password is a certification process, after a successful login authentication system will allow users to access resources in their accounts, which is called authorization. The situation is more complex concept of user roles will be different for each role have permissions to the user given a role in the process is an authorization procedure.

User's login state on the server side is divided into state and non-state modes, in the era of the monomer distributed architecture, we Session in order to allow sharing of information between multiple Tomcat instances, the usual solution is to store Session to a cache database. I.e., the figure Session Store, this may be a Session Store Redis may be MemCache, this model is to have the state:
User authentication and authorization services micro Zatan

The reason why there is a state, because of the need to maintain the server, the Session store information that the user's login state is actually maintained on the server side, so the server can be learned for the user's login state at any time, and the user the Session has a relatively high control. The main drawback of stateful mode is that this Session Store, as Session Store service if only one node, so that when business expansion, user amount increases there will be a performance bottleneck, and data migration too much trouble. Of course, you can choose to add nodes, you just need to put the cost of the corresponding machine.

Another non-state mode, refer to the server not to record the user's login status, that is, the server is no longer to maintain a Session. But when the user logs in successfully, issue a token to the client, each request after the client needs to carry token. When a client requests the server would be carried by the token to decrypt, verify token is legitimate and whether it has expired, and so on. After successful verification token is considered to be a user login state, or that the user is not logged:
User authentication and authorization services micro Zatan

Note: token unique ID is usually stored in the user's decryption token is to get the user ID and then query the database to cache or user data. Of course, you can choose the user data are stored in the token, the only way this can be a security problem or data consistency problems

无状态模式下的token其实和有状态模式下的session作用是类似的,都是判断用户是否具有登录态的一个凭证。只不过在无状态模式下,服务器端不需要再去维护、存储一个Session,只需要对客户端携带的token进行解密和校验。也就是说存储实际是交给了客户端完成,所以无状态的优点恰恰就是弥补了有状态的缺点。但是无状态的缺点也很明显,因为一旦把token交给客户端后,服务端就无法去控制这个token了。例如想要强制下线某个用户在无状态的模式下就比较难以实现。

有状态与无状态各有优缺点,只不过目前业界趋势更倾向于无状态:

优缺点 有状态 无状态
优点 服务端控制能力强 去中心化,无存储,简单,任意扩容、缩容
缺点 存在中心点,鸡蛋放在一个篮子里,迁移麻烦。服务端存储数据,加大了服务端压力 服务端控制能力相对弱

微服务认证方案

微服务认证方案有很多种,需要根据实际的业务需求定制适合自己业务的方案,这里简单列举一下业界内常用的微服务认证方案。

1、“处处安全” 方案:

所谓“处处安全” 方案,就是考虑了微服务认证中的方方面面,这种方案主流是使用OAuth2协议进行实现。这种方案的优点是安全性好,但是实现的成本及复杂性比较高。另外,多个微服务之间互相调用需要传递token,所以会发生多次认证,有一定的性能开销

OAuth2的代表实现框架:

参考文章:

2、外部无状态,内部有状态方案:

这种方案虽然看着有些奇葩,但是也许多公司在使用。在该方案下,网关不存储Session,而是接收一个token和JSESSIONID,网关仅对token进行解密、校验,然后将JSESSIONID转发到其代理的微服务上,这些微服务则是通过JSESSIONID从Session Store获取共享Session。如下图:
User authentication and authorization services micro Zatan

这种方案主要是出现在内部有旧的系统架构的情况,在不重构或者没法全部重构的前提下为了兼容旧的系统,就可以采用该方案。而且也可以将新旧系统分为两块,网关将token和JSESSIONID一并转发到下游服务,这样无状态模式的系统则使用token,有状态模式的系统则使用Session,然后再慢慢地将旧服务进行重构以此实现一个平滑过渡。如下图:
User authentication and authorization services micro Zatan

3、“网关认证授权,内部裸奔” 方案:

在该方案下,认证授权在网关完成,下游的微服务不需要进行认证授权。网关接收到客户端请求所携带的token后,对该token进行解密和校验,然后将解密出来的用户信息转发给下游微服务。这种方案的优点是实现简单、性能也好,缺点是一旦网关被攻破,或者能越过网关访问微服务就会有安全问题。如下图:
User authentication and authorization services micro Zatan

4、“内部裸奔” 改进方案:

上一个方案的缺陷比较明显,我们可以对该方案进行一些改进,例如引入一个认证授权中心服务,让网关不再做认证和授权以及token的解密和解析。用户的登录请求通过网关转发到认证授权中心完成登录,登录成功后由认证授权中心颁发token给客户端。客户端每次请求都携带token,而每个微服务都需要对token进行解密和解析,以确定用户的登录态。改进之后所带来的好处就是网关不再关注业务,而是单纯的请求做转发,可以在一定程度上解耦业务,并且也更加安全,因为每个微服务不再裸奔而是都需要验证请求中所携带的token。如下图:
User authentication and authorization services micro Zatan

5、方案的对比与选择:

以上所提到的常见方案只是用于抛砖引玉,没有哪个方案是绝对普适的。而且实际开发中通常会根据业务改进、组合这些方案演变出不同的变种,所以应该要学会活学活用而不是局限于某一种方案。下面简单整理了一下这几种方案,以便做对比:
User authentication and authorization services micro Zatan

6、访问控制模型

了解了常见的微服务认证方案后,我们来简单看下访问控制模型。所谓访问控制,就是用户需要满足怎么样的条件才允许访问某个系统资源,即控制系统资源的访问权限。访问控制模型主要有以下几种:

  1. Access Control List(ACL,访问控制列表):

    在该模型下的一个系统资源会包含一组权限列表,该列表规定了哪些用户拥有哪些操作权限。例如有一个系统资源包含的权限列表为:[Alice: read, write; Bob: read];那么就表示Alice这个用户对该资源拥有read和write权限,而Bob这个用户则对该资源拥有read权限。该模型通常用于文件系统

  2. Role-based access control(RBAC,基于角色的访问控制):

    即用户需关联一个预先定义的角色,而不同的角色拥有各自的权限列表。用户登录后只需要查询其关联的角色就能查出该用户拥有哪些权限。例如用户A关联了一个名为观察者的角色,该角色下包含接口A和接口B的访问权限,那么就表示用户A仅能够访问A和接口B。该模型在业务系统中使用得最多

  3. Attribute-based access control(ABAC,基于属性的访问控制):

    在该模型下,用户在访问某个系统资源时会携带一组属性值包括自身属性、主题属性、资源属性以及环境属性等。然后系统通过动态计算用户所携带的属性来判断是否满足具有访问某个资源的权限。属性通常来说分为四类:用户属性(如用户年龄),环境属性(如当前时间),操作属性(如读取)以及对象属性等。

    为了能让系统进行权限控制,在该模型下需要以特定的格式定义权限规则,例如:IF 用户是管理员; THEN 允许对敏感数据进行读/写操作。在这条规则中“管理员”是用户的角色属性,而“读/写”是操作属性,”敏感数据“则是对象属性。

    ABAC有时也被称为PBAC(Policy-Based Access Control,基于策略的访问控制)或CBAC(Claims-Based Access Control,基于声明的访问控制)。该模型由于比较复杂,使用得不多,k8s也因为ABAC太复杂而在1.8版本改为使用RBAC模型

  4. Rules-based access control(RBAC,基于规则的访问控制):

    在该模型下通过对某个系统资源事先定义一组访问规则来实现访问控制,这些规则可以是参数、时间、用户信息等。例如:只允许从特定的IP地址访问或拒绝从特定的IP地址访问

  5. Time-based access control list(TBACL,基于时间的访问控制列表):

    该模型是在ACL的基础上添加了时间的概念,可以设置ACL权限在特定的时间才生效。例如:只允许某个系统资源在工作日时间内才能被外部访问,那么就可以将该资源的ACL权限的有效时间设置为工作日时间内


JWT

之前提到过无状态模式下,服务器端需要生成一个Token颁发给客户端,而目前主流的方式就是使用JWT的标准来生成Token,所以本小节我们来简单了解下JWT及其使用。

JWT简介:

JWT是JSON Web Token的缩写,JWT实际是一个开放标准(RFC 7519),用来在各方之间安全地传输信息,是目前最流行的跨域认证解决方案。JWT可以被验证和信任,因为它是数字签名的。官网:https://jwt.io/

JWT的组成结构:

组成 作用 内容示例
Header(头) 记录Token类型、签名的算法等 {"alg": "HS256", "type": "JWT"}
Payload(有效载荷) 携带一些用户信息及Token的过期时间等 {"user_id": "1", "iat": 1566284273, "exp": 1567493873}
Signature(签名) 签名算法生成的数字签名,用于防止Token被篡改、确保Token的安全性 WV5Hhymti3OgIjPprLJKJv3aY473vyxMLeM8c7JLxSk

JWT生成Token的公式:

Token = Base64(Header).Base64(Payload).Base64(Signature)

示例:eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjEiLCJpYXQiOjE1NjYyODIyMjMsImV4cCI6MTU2NzQ5MTgyM30.OtCOFqWMS6ZOzmwCs7NC7hs9u043P-09KbQfZBov97E

签名是使用Header里指定的签名算法生成的,公式如下:

Signature = Signature Algorithm ((Base64 (Header) .Base64 (Payload), secret key))


Use JWT:

1, the current third-party libraries Java language has several operating JWT, where the use of one of the more lightweight jjwt as a demonstration. First, add a dependency as follows:

<dependency>
  <groupId>io.jsonwebtoken</groupId>
  <artifactId>jjwt-api</artifactId>
  <version>0.10.7</version>
</dependency>
<dependency>
  <groupId>io.jsonwebtoken</groupId>
  <artifactId>jjwt-impl</artifactId>
  <version>0.10.7</version>
  <scope>runtime</scope>
</dependency>
<dependency>
  <groupId>io.jsonwebtoken</groupId>
  <artifactId>jjwt-jackson</artifactId>
  <version>0.10.7</version>
  <scope>runtime</scope>
</dependency>

2, a write tools, the operations are extracted JWT, easy to use in the project. Specific code as follows:

package com.zj.node.usercenter.util;

import io.jsonwebtoken.*;
import io.jsonwebtoken.security.Keys;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import javax.crypto.SecretKey;
import java.util.Date;
import java.util.Map;

/**
 * JWT 工具类
 *
 * @author 01
 * @date 2019-08-20
 **/
@Slf4j
@Component
@RequiredArgsConstructor
@SuppressWarnings("WeakerAccess")
public class JwtOperator {
    /**
     * 秘钥
     * - 默认5d1IB9SiWd5tjBx&EMi^031CtigL!6jJ
     */
    @Value("${jwt.secret:5d1IB9SiWd5tjBx&EMi^031CtigL!6jJ}")
    private String secret;
    /**
     * 有效期,单位秒
     * - 默认2周
     */
    @Value("${jwt.expire-time-in-second:1209600}")
    private Long expirationTimeInSecond;

    /**
     * 从token中获取claim
     *
     * @param token token
     * @return claim
     */
    public Claims getClaimsFromToken(String token) {
        try {
            return Jwts.parser()
                    .setSigningKey(this.secret.getBytes())
                    .parseClaimsJws(token)
                    .getBody();
        } catch (ExpiredJwtException | UnsupportedJwtException |
                MalformedJwtException | IllegalArgumentException e) {
            log.error("token解析错误", e);
            throw new IllegalArgumentException("Token invalided.");
        }
    }

    /**
     * 获取token的过期时间
     *
     * @param token token
     * @return 过期时间
     */
    public Date getExpirationDateFromToken(String token) {
        return getClaimsFromToken(token)
                .getExpiration();
    }

    /**
     * 判断token是否过期
     *
     * @param token token
     * @return 已过期返回true,未过期返回false
     */
    private Boolean isTokenExpired(String token) {
        Date expiration = getExpirationDateFromToken(token);
        return expiration.before(new Date());
    }

    /**
     * 计算token的过期时间
     *
     * @return 过期时间
     */
    private Date getExpirationTime() {
        return new Date(System.currentTimeMillis() + this.expirationTimeInSecond * 1000);
    }

    /**
     * 为指定用户生成token
     *
     * @param claims 用户信息
     * @return token
     */
    public String generateToken(Map<String, Object> claims) {
        Date createdTime = new Date();
        Date expirationTime = this.getExpirationTime();

        byte[] keyBytes = secret.getBytes();
        SecretKey key = Keys.hmacShaKeyFor(keyBytes);

        return Jwts.builder()
                .setClaims(claims)
                .setIssuedAt(createdTime)
                .setExpiration(expirationTime)
                // 你也可以改用你喜欢的算法
                // 支持的算法详见:https://github.com/jwtk/jjwt#features
                .signWith(key, SignatureAlgorithm.HS256)
                .compact();
    }

    /**
     * 判断token是否非法
     *
     * @param token token
     * @return 未过期返回true,否则返回false
     */
    public Boolean validateToken(String token) {
        return !isTokenExpired(token);
    }
}

3, if the default configuration does not meet the requirements, you can customize by adding the following configuration in the configuration file:

jwt:
  # 秘钥
  secret: 5d1IB9SiWd5tjBx&EMi^031CtigL!6jJ
  # jwt有效期,单位秒
  expire-time-in-second: 1209600

4, after completing the above steps, can be used in the project JWT, there is provided a more comprehensive test, reference may be used in the test tools. code show as below:

package com.zj.node.usercenter.util;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.security.SignatureException;
import org.apache.tomcat.util.codec.binary.Base64;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;

import java.util.HashMap;
import java.util.Map;

/**
 * JwtOperator 测试用例
 *
 * @author 01
 * @date 2019-08-20
 **/
@SpringBootTest
@RunWith(SpringRunner.class)
public class JwtOperatorTests {

    @Autowired
    private JwtOperator jwtOperator;

    private String token = "";

    @Before
    public void generateTokenTest() {
        // 设置用户信息
        Map<String, Object> objectObjectHashMap = new HashMap<>();
        objectObjectHashMap.put("id", "1");

        // 测试1: 生成token
        this.token = jwtOperator.generateToken(objectObjectHashMap);
        // 会生成类似该字符串的内容: eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjEiLCJpYXQiOjE1NjU1ODk4MTcsImV4cCI6MTU2Njc5OTQxN30.27_QgdtTg4SUgxidW6ALHFsZPgMtjCQ4ZYTRmZroKCQ
        System.out.println(this.token);
    }

    @Test
    public void validateTokenTest() {
        // 测试2: 如果能token合法且未过期,返回true
        Boolean validateToken = jwtOperator.validateToken(this.token);
        System.out.println("token校验结果:" + validateToken);
    }

    @Test
    public void getClaimsFromTokenTest() {
        // 测试3: 解密token,获取用户信息
        Claims claims = jwtOperator.getClaimsFromToken(this.token);
        System.out.println(claims);
    }

    @Test
    public void decodeHeaderTest() {
        // 获取Header,即token的第一段(以.为边界)
        String[] split = this.token.split("\\.");
        String encodedHeader = split[0];

        // 测试4: 解密Header
        byte[] header = Base64.decodeBase64(encodedHeader.getBytes());
        System.out.println(new String(header));
    }

    @Test
    public void decodePayloadTest() {
        // 获取Payload,即token的第二段(以.为边界)
        String[] split = this.token.split("\\.");
        String encodedPayload = split[1];

        // 测试5: 解密Payload
        byte[] payload = Base64.decodeBase64(encodedPayload.getBytes());
        System.out.println(new String(payload));
    }

    @Test(expected = SignatureException.class)
    public void validateErrorTokenTest() {
        try {
            // 测试6: 篡改原本的token,因此会报异常,说明JWT是安全的
            jwtOperator.validateToken(this.token + "xx");
        } catch (SignatureException e) {
            e.printStackTrace();
            throw e;
        }
    }
}

If you want to know all kinds of JWT library, you can refer to the following articles:


Use JWT implement authentication and authorization

Understanding of JWT, we use a JWT implement authentication and authorization Demo, first define a DTO, its structure is as follows:

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class LoginRespDTO {
    /**
     * 昵称
     */
    private String userName;

    /**
     * token
     */
    private String token;

    /**
     * 过期时间
     */
    private Long expirationTime;
}

Then write Service, provides a method of simulation and modeling login checks the user's login state. Specific code as follows:

@Slf4j
@Service
@RequiredArgsConstructor
public class UserService {

    private final JwtOperator jwtOperator;

    /**
     * 模拟用户登录
     */
    public LoginRespDTO login(String userName, String password) {
        String defPassword = "123456";
        if (!defPassword.equals(password)) {
            return null;
        }

        // 密码验证通过颁发token
        Map<String, Object> userInfo = new HashMap<>();
        userInfo.put("userName", userName);
        String token = jwtOperator.generateToken(userInfo);

        return LoginRespDTO.builder()
                .userName(userName)
                .token(token)
                .expirationTime(jwtOperator.getExpirationDateFromToken(token).getTime())
                .build();
    }

    /**
     * 模拟登录态验证
     */
    public String checkLoginState(String token) {
        if (jwtOperator.validateToken(token)) {
            Claims claims = jwtOperator.getClaimsFromToken(token);
            String userName = claims.get("userName").toString();

            return String.format("用户 %s 的登录态验证通过,允许访问", userName);
        }

        return "登录态验证失败,token无效或过期";
    }
}

It followed Controller layer open the corresponding Web Interface. code show as below:

@Slf4j
@RestController
@RequestMapping("/users")
@RequiredArgsConstructor
public class UserController {

    private final UserService userService;

    @PostMapping("/login")
    public LoginRespDTO login(@RequestParam("userName") String userName,
                              @RequestParam("password") String password) {
        return userService.login(userName, password);
    }

    @GetMapping("/checkLoginState")
    public String checkLoginState(@RequestParam("token") String token) {
        return userService.checkLoginState(token);
    }
}

User login is successful, return Token and basic user information:
User authentication and authorization services micro Zatan

Check login state:
User authentication and authorization services micro Zatan

Tips:

This section only gives an example of a minimalist, the purpose is to demonstrate how to use JWT achieve success after the user logs on to the client as well as Token issued by Token verify the user's login state, so we can mentioned before the adoption of the program to expand . Typically Token issued to the client, the client is the HTTP Header in Token passed on in a subsequent request, rather than the parameters passed in the example. Token passing between the micro-services, too, a micro service before requesting the service to another micro, need to first Token into this request in the HTTP Header. Further, Token validation logic is typically placed in a global filter or interceptor, so that there is no need to write again to verify that each interface logic.

Guess you like

Origin blog.51cto.com/zero01/2435946