开放平台之安全
什么是开放平台
开放平台就是将企业中的业务的核心部分经过抽象和提取,形成面向企业或者面向用户的增值系统,为企业带来新的业务增涨点。
因为是企业的核心业务能力,所以平台的安全性就成为重中之重。
安全方案
普通的接口使用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并且登录成功时,只能访问普通用户权限的接口,不能访问管理用户权限的接口。
总结
关于开放平台其实还有很多需要切入的点,此处给出的安全方案只是一个示例,可以在此基础上进行二次开发,实现企业级的安全方案。文中的示例代码地址如下: