什么是開放平台
開放平台就是將企業中的業務的核心部分經過抽象和提取,形成面向企業或者面向用戶的增值系統,為企業帶來新的業務增漲點。
因為是企業的核心業務能力,所以平台的安全性就成為重中之重。
安全方案
普通的接口使用Token令牌的方案就可以保證,但是對於一些敏感的接口就需要有針對性的處理,比如使用https。
https是在http超文本傳輸協議加入SSL層,它在網絡間通信是加密的,所以需要加密證書。
https協議需要ca證書,一般需要交費。
簽名的設計一般是通過用戶和密碼的校驗,然后針對用戶生成一個唯一的Token令牌,
用戶再次獲取信息時,帶上此令牌,如果令牌正確,則返回數據。對於獲取Token信息后,訪問用戶相關接口,客戶端請求的url需要帶上如下參數:
時間戳:timestamp
Token令牌:token
jwt
JWT(json web token)是為了在網絡應用環境間傳遞聲明而執行的一種基於JSON的開放標准。
JWT的聲明一般被用來在身份提供者和服務提供者間傳遞被認證的用戶身份信息,以便於從資源服務器獲取資源。比如用在用戶登錄上。
那么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的工作流程圖。
- 用戶導航到登錄頁,輸入用戶名、密碼,進行登錄
- 服務器驗證登錄鑒權,如果改用戶合法,根據用戶的信息和服務器的規則生成JWT Token
- 服務器將該token以json形式返回(不一定要json形式,這里說的是一種常見的做法)
- 用戶得到token,存在localStorage、cookie或其它數據存儲形式中。
- 以后用戶請求
/protected
中的API時,在請求的header中加入Authorization: Bearer xxxx(token)
。此處注意token之前有一個7字符長度的Bearer
- 服務器端對此token進行檢驗,如果合法就解析其中內容,根據其擁有的權限和自己的業務邏輯給出對應的響應結果。
- 用戶取得結果
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,我們能夠看到測試頁面
當輸入用戶名為admin並且登錄成功時,點擊右側的按鈕能夠調用相應的接口。當登錄不成功時,會返回401錯誤。
當輸入用戶名為user並且登錄成功時,只能訪問普通用戶權限的接口,不能訪問管理用戶權限的接口。
總結
關於開放平台其實還有很多需要切入的點,此處給出的安全方案只是一個示例,可以在此基礎上進行二次開發,實現企業級的安全方案。文中的示例代碼地址如下: