開放平臺之安全

什麼是開放平臺

開放平臺就是將企業中的業務的核心部分通過抽象和提取,造成面向企業或者面向用戶的增值系統,爲企業帶來新的業務增漲點。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

相關文章
相關標籤/搜索