开放平台之安全

什么是开放平台

开放平台就是将企业中的业务的核心部分通过抽象和提取,造成面向企业或者面向用户的增值系统,为企业带来新的业务增涨点。java

由于是企业的核心业务能力,因此平台的安全性就成为重中之重。mysql

image

安全方案

普通的接口使用Token令牌的方案就能够保证,可是对于一些敏感的接口就须要有针对性的处理,好比使用https。git

https是在http超文本传输协议加入SSL层,它在网络间通讯是加密的,因此须要加密证书。github

https协议须要ca证书,通常须要交费。web

签名的设计通常是经过用户和密码的校验,而后针对用户生成一个惟一的Token令牌,redis

用户再次获取信息时,带上此令牌,若是令牌正确,则返回数据。对于获取Token信息后,访问用户相关接口,客户端请求的url须要带上以下参数:算法

         时间戳:timestampspring

         Token令牌:tokensql

jwt

JWT(json web token)是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准。json

JWT的声明通常被用来在身份提供者和服务提供者间传递被认证的用户身份信息,以便于从资源服务器获取资源。好比用在用户登陆上。

image

那么jwt到底长什么样呢?

第一部分咱们称它为头部(header),第二部分咱们称其为载荷(payload),第三部分是签证(signature)。

header

jwt的头部承载两部分信息:

  • 声明类型,这里是jwt
  • 声明加密的算法 一般直接使用 HMAC SHA256

完整的头部就像下面这样的JSON:

{

"typ": "JWT",

"alg": "HS256"

}

而后将头部进行base64加密(该加密是能够对称解密的),构成了第一部分:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9

playload

载荷就是存放有效信息的地方。这个名字像是特指飞机上承载的货品,这些有效信息包含三个部分

  • 标准中注册的声明
  • 公共的声明
  • 私有的声明

标准中注册的声明 (建议但不强制使用) :

  • iss: jwt签发者
  • sub: jwt所面向的用户
  • aud: 接收jwt的一方
  • exp: jwt的过时时间,这个过时时间必需要大于签发时间
  • nbf: 定义在什么时间以前,该jwt都是不可用的.
  • iat: jwt的签发时间
  • jti: jwt的惟一身份标识,主要用来做为一次性token,从而回避重放攻击。

公共的声明 :

公共的声明能够添加任何的信息,通常添加用户的相关信息或其余业务须要的必要信息.但不建议添加敏感信息,由于该部分在客户端可解密.

私有的声明 :

私有声明是提供者和消费者所共同定义的声明,通常不建议存放敏感信息,由于base64是对称解密的,意味着该部分信息能够归类为明文信息。

定义一个payload:

{

"name":"Free码农",

"age":"28",

"org":"今日头条"

}

而后将其进行base64加密,获得Jwt的第二部分:

eyJvcmciOiLku4rml6XlpLTmnaEiLCJuYW1lIjoiRnJlZeeggeWGnCIsImV4cCI6MTUxNDM1NjEwMywiaWF0IjoxNTE0MzU2MDQzLCJhZ2UiOiIyOCJ9

signature

jwt的第三部分是一个签证信息,这个签证信息由三部分组成:

  • header (base64后的)

  • payload (base64后的)

  • secret

这个部分须要base64加密后的header和base64加密后的payload使用.链接组成的字符串,而后经过header中声明的加密方式进行加盐secret组合加密,而后就构成了jwt的第三部分:

49UF72vSkj-sA4aHHiYN5eoZ9Nb4w5Vb45PsLF7x_NY

密钥secret是保存在服务端的,服务端会根据这个密钥进行生成token和验证,因此须要保护好。

jwt工做流程

下面是一个JWT的工做流程图。

  1. 用户导航到登陆页,输入用户名、密码,进行登陆
  2. 服务器验证登陆鉴权,若是改用户合法,根据用户的信息和服务器的规则生成JWT Token
  3. 服务器将该token以json形式返回(不必定要json形式,这里说的是一种常见的作法)
  4. 用户获得token,存在localStorage、cookie或其它数据存储形式中。
  5. 之后用户请求/protected中的API时,在请求的header中加入 Authorization: Bearer xxxx(token)。此处注意token以前有一个7字符长度的 Bearer
  6. 服务器端对此token进行检验,若是合法就解析其中内容,根据其拥有的权限和本身的业务逻辑给出对应的响应结果。
  7. 用户取得结果

image


spring boot整合jwt

首先,加入依赖

                <dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-data-jpa</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-data-redis</artifactId>
		</dependency>
		<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.security</groupId>
			<artifactId>spring-security-test</artifactId>
			<scope>test</scope>
		</dependency>

		<!-- https://mvnrepository.com/artifact/mysql/mysql-connector-java -->
		<dependency>
			<groupId>mysql</groupId>
			<artifactId>mysql-connector-java</artifactId>
			<version>5.1.45</version>
		</dependency>

		<!-- https://mvnrepository.com/artifact/io.jsonwebtoken/jjwt -->
		<dependency>
			<groupId>io.jsonwebtoken</groupId>
			<artifactId>jjwt</artifactId>
			<version>0.9.0</version>
		</dependency>

配置信息代码以下:

# JACKSON
spring:
  jackson:
    serialization:
      INDENT_OUTPUT: true

jwt:
  header: Authorization
  secret: mySecret
  expiration: 604800
  route:
    authentication:
      path: auth
      refresh: refresh

token处理类为JwtTokenUtil,代码以下:

@Component
public class JwtTokenUtil implements Serializable {

    private static final long serialVersionUID = -3301605591108950415L;

    static final String CLAIM_KEY_USERNAME = "sub";
    static final String CLAIM_KEY_AUDIENCE = "aud";
    static final String CLAIM_KEY_CREATED = "iat";

    static final String AUDIENCE_UNKNOWN = "unknown";
    static final String AUDIENCE_WEB = "web";
    static final String AUDIENCE_MOBILE = "mobile";
    static final String AUDIENCE_TABLET = "tablet";

    @Autowired
    private TimeProvider timeProvider;

    @Value("${jwt.secret}")
    private String secret;

    @Value("${jwt.expiration}")
    private Long expiration;

    public String getUsernameFromToken(String token) {
        return getClaimFromToken(token, Claims::getSubject);
    }

    public Date getIssuedAtDateFromToken(String token) {
        return getClaimFromToken(token, Claims::getIssuedAt);
    }

    public Date getExpirationDateFromToken(String token) {
        return getClaimFromToken(token, Claims::getExpiration);
    }

    public String getAudienceFromToken(String token) {
        return getClaimFromToken(token, Claims::getAudience);
    }

    public <T> T getClaimFromToken(String token, Function<Claims, T> claimsResolver) {
        final Claims claims = getAllClaimsFromToken(token);
        return claimsResolver.apply(claims);
    }

    private Claims getAllClaimsFromToken(String token) {
        return Jwts.parser()
                .setSigningKey(secret)
                .parseClaimsJws(token)
                .getBody();
    }

    private Boolean isTokenExpired(String token) {
        final Date expiration = getExpirationDateFromToken(token);
        return expiration.before(timeProvider.now());
    }

    private Boolean isCreatedBeforeLastPasswordReset(Date created, Date lastPasswordReset) {
        return (lastPasswordReset != null && created.before(lastPasswordReset));
    }

    private String generateAudience(Device device) {
        String audience = AUDIENCE_UNKNOWN;
        if (device.isNormal()) {
            audience = AUDIENCE_WEB;
        } else if (device.isTablet()) {
            audience = AUDIENCE_TABLET;
        } else if (device.isMobile()) {
            audience = AUDIENCE_MOBILE;
        }
        return audience;
    }

    private Boolean ignoreTokenExpiration(String token) {
        String audience = getAudienceFromToken(token);
        return (AUDIENCE_TABLET.equals(audience) || AUDIENCE_MOBILE.equals(audience));
    }

    public String generateToken(UserDetails userDetails, Device device) {
        Map<String, Object> claims = new HashMap<>();
        return doGenerateToken(claims, userDetails.getUsername(), generateAudience(device));
    }

    private String doGenerateToken(Map<String, Object> claims, String subject, String audience) {
        final Date createdDate = timeProvider.now();
        final Date expirationDate = calculateExpirationDate(createdDate);

        System.out.println("doGenerateToken " + createdDate);

        return Jwts.builder()
                .setClaims(claims)
                .setSubject(subject)
                .setAudience(audience)
                .setIssuedAt(createdDate)
                .setExpiration(expirationDate)
                .signWith(SignatureAlgorithm.HS512, secret)
                .compact();
    }

    public Boolean canTokenBeRefreshed(String token, Date lastPasswordReset) {
        final Date created = getIssuedAtDateFromToken(token);
        return !isCreatedBeforeLastPasswordReset(created, lastPasswordReset)
                && (!isTokenExpired(token) || ignoreTokenExpiration(token));
    }

    public String refreshToken(String token) {
        final Date createdDate = timeProvider.now();
        final Date expirationDate = calculateExpirationDate(createdDate);

        final Claims claims = getAllClaimsFromToken(token);
        claims.setIssuedAt(createdDate);
        claims.setExpiration(expirationDate);

        return Jwts.builder()
                .setClaims(claims)
                .signWith(SignatureAlgorithm.HS512, secret)
                .compact();
    }

    public Boolean validateToken(String token, UserDetails userDetails) {
        JwtUser user = (JwtUser) userDetails;
        final String username = getUsernameFromToken(token);
        final Date created = getIssuedAtDateFromToken(token);
        //final Date expiration = getExpirationDateFromToken(token);
        return (
              username.equals(user.getUsername())
                    && !isTokenExpired(token)
                    && !isCreatedBeforeLastPasswordReset(created, user.getLastPasswordResetDate())
        );
    }

    private Date calculateExpirationDate(Date createdDate) {
        return new Date(createdDate.getTime() + expiration * 1000);
    }
}

最后,在控制层对token的处理进行调用,就可以完成用户的权限认证。

测试

启动应用,而后输入http://localhost:8080,咱们可以看到测试页面

image

当输入用户名为admin而且登陆成功时,点击右侧的按钮可以调用相应的接口。当登陆不成功时,会返回401错误。

当输入用户名为user而且登陆成功时,只能访问普通用户权限的接口,不能访问管理用户权限的接口。

总结

关于开放平台其实还有不少须要切入的点,此处给出的安全方案只是一个示例,能够在此基础上进行二次开发,实现企业级的安全方案。文中的示例代码地址以下:

https://github.com/cloudskyme/jwt-spring-security-demo

相关文章
相关标签/搜索