写在前面
我们在前两篇文章的基础上,继续探讨APP服务端的高级配置。那么,我们在此主要探讨的内容有:
什么是REST
REST(Representational State Transfer)是一种软件架构风格。它将服务端的信息和功能等所有事物统称为资源,客户端的请求实际就是对资源进行操作。
它的主要特点有: – 每一个资源都会对应一个独一无二的url – 客户端通过HTTP的GET、POST、PUT、DELETE请求方法对资源进行查询、创建、修改、删除操作 – 客户端与服务端的交互必须是无状态的。
什么是RESTful架构:
- 每一个URI代表一种资源;
- 客户端和服务器之间,传递这种资源的某种表现层;
- 客户端通过四个HTTP动词,对服务器端资源进行操作,实现”表现层状态转化”。
传统的身份认证方法
Http是一种没有状态的协议,也就是服务端不知道谁是访问的应用。把用户看成是客户端,客户端使用用户名和密码进行身份认证,
不过这个客户端再次发送请求,还要验证。解决的方法是,当用户请求登录的时候,如果登录成功,那么在服务端生成一条记录
这条记录用于说明登录用户是谁,然后把这条记录的ID号发给客户端,客户端收到以后把这个ID存储到cookie中,下次这个用户
再向服务器发送请求的时候,可以带着这个Cookie,这样服务端验证一下这个Cookie里的信息,看看能不能在服务端找到对应的纪录,
如果可以,说明用户已经通过了身份验证,把用户请求的数据返回给客户端。
上面说的就是Session,服务端存储为登录的用户生成的Session,这些Session可能存储在内存,磁盘或者数据库中,并且需要服务端定期的清理过期的Session。
基于Token的身份认证
使用基于Token的身份认证,大概的流程是:
- 客户端使用用户名和密码请求登录
- 服务端接收请求,去验证用户名与密码
- 验证成功,服务端签发一个Token(有效期较短)和一个RefreshToken(有效期较长),再把Token和RefreshToken发送给客户端
- 客户端接收到Token和RefreshToken后,再把它们存储起来
- 客户端每次向服务端请求资源的时候需要带着服务端签发的Token
- 服务端接收到请求,然后去验证客户端请求里面的Token
- 如果验证成功,则向客户端返回请求结果;如果验证不成功,则用RefreshToken向服务器请求新的Token
- 服务端接收到请求,然后去验证RefreshToken
- 如果验证成功,则签发一个新的Token,把Token发给客户端;如果验证不成功,则告诉客户端refreshToken失效,重新登录
Token的生成之JWT
Token的生成方式有很多种,比较热门的有JWT(JSON WEB TOKEN),OAuth等。那我们采用JWT来生成我们的Token。
JWT 标准的 Token 有三个部分:
- header
- payload
- signature
中间用点分隔开,并且都会使用Base64编码,其形式为:
1 | eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ |
Header
header 部分主要是两部分内容,一个是Token的类型,另一个是使用的算法,形式如下:
1 | { |
上面的内容用Base64的形式编码一下,变成:
1 | eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9 |
Payload
Payload 里面是Token的具体内容,这些内容里面有一些事标准字段,同时可以添加自己需要的内容,标准字段如下:
- iss: Issuer,发行者
- sub: Subject,主题
- aub: Audience,观众
- exp,Expiration time,过期时间
- nbf: Not before
- iat: Issued at,发行时间
jti: JWT id
比如下面的Payload,用到了sub,另外有两个自定义的字段,一个是name,还有一个是admin。
1
2
3
4
5{
"sub": "1234567890",
"name": "John Doe",
"admin": true
}使用Base64编码以后变成这个样子:
1
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9
Signature
JWT 的最后一部分是Signature,这部分内容有三个部分,先是用Base64编码的header.payload,再用加密算法加密一下,加密的时候
要放进去一个Secret,这个相当于一个密码,这个密码秘密地存储在服务端。- header
- payload
- secret
1
HMACSHA256(base64UrlEncode(header) + "." +base64UrlEncode(payload), 'secret')
处理完成以后看起来像这样:
1 | TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ |
最后这个在服务端生成并且要发送给客户端的 Token 看起来像这样:
1 | eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ |
客户端收到这个 Token 以后把它存储下来,下回向服务端发送请求的时候就带着这个 Token 。服务端收到这个 Token ,然后进行验证,通过以后就会返回给客户端想要的资源。
程序示例
- JWT的生成
JWT的生成,我们使用的是一个Java的开源库jjwt,添加引用
1 | compile 'io.jsonwebtoken:jjwt:0.7.0' |
同时使用该库,创建一个工具类,用于对token创建,检验等操作
1 |
|
创建一个TokenModel的类,存储于JWT的Subject1
2
3
4
5public class TokenModel {
private String userId;
private String roleId;
//getter和setter省略
}
Token的创建与检测的类1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
public class TokenServiceImpl implements TokenService{
private StringRedisTemplate redisTemplate;
private JwtUtil jwtUtil;
private final Logger logger = LoggerFactory.getLogger(this.getClass());
public String createAccessToken(String userId) {
String access_token=null;
TokenModel tokenModel=new TokenModel(userId);
try {
//生成Token
access_token = jwtUtil.createJWT(Constant.JWT_ID, JSONUtils.toJson(tokenModel), Constant.JWT_TTL);
//保存登录状态(存储于redis服务器中)
redisTemplate.boundValueOps(RedisUtils.generateTokenKey(userId)).set(access_token,Constant.JWT_TTL, TimeUnit.MILLISECONDS);
}catch (Exception e){
logger.error("---TokenServiceImpl---createToken---",e);
}
return access_token;
}
public String createRefreshToken(String userId) {
String refreshToken=null;
TokenModel tokenModel=new TokenModel(userId);
try {
//生成Token
refreshToken = jwtUtil.createJWT(Constant.JWT_ID, JSONUtils.toJson(tokenModel), Constant.JWT_REFRESH_TTL);
//保存登录状态
redisTemplate.boundValueOps(RedisUtils.generateRefreshTokenKey(userId)).set(refreshToken,Constant.JWT_REFRESH_TTL, TimeUnit.MILLISECONDS);
}catch (Exception e){
logger.error("---TokenServiceImpl---createToken---",e);
}
return refreshToken;
}
public boolean checkToken(String token) {
if(token==null){
return false;
}
Claims claims = jwtUtil.parseJWT(token);//解析获取token中的userId
if(claims!=null){
long expireTime=claims.getExpiration().getTime();
long currentTime=System.currentTimeMillis();
if(currentTime<expireTime){//表示已过期
TokenModel tokenModel=JSONUtils.fromJson(claims.getSubject(),TokenModel.class);
String userId=tokenModel.getUserId();
String redisToken=redisTemplate.opsForValue().get(RedisUtils.generateTokenKey(userId));
if(TextUtils.equals(token,redisToken)){//相同
//如果验证成功,说明此用户进行了一次有效操作,延长token的过期时间
//redisTemplate.boundValueOps(RedisUtils.generateTokenKey(userId)).expire(Constant.JWT_TTL, TimeUnit.MILLISECONDS);
return true;
}
}
}
return false;
}
public boolean checkRefreshToken(String refresh_token) {
if(refresh_token==null){
return false;
}
Claims claims = jwtUtil.parseJWT(refresh_token);//解析获取token中的userId
if(claims!=null){
long expireTime=claims.getExpiration().getTime();
long currentTime=System.currentTimeMillis();
if(currentTime<expireTime) {//表示已过期
TokenModel tokenModel=JSONUtils.fromJson(claims.getSubject(),TokenModel.class);
String userId=tokenModel.getUserId();
String redisToken=redisTemplate.opsForValue().get(RedisUtils.generateRefreshTokenKey(userId));
if(TextUtils.equals(refresh_token,redisToken)) {//相同
return true;
}
}
}
return false;
}
public void deleteToken(String userId) {
redisTemplate.delete(RedisUtils.generateTokenKey(userId));
}
public void deleteRefreshToken(String userId) {
redisTemplate.delete(RedisUtils.generateRefreshTokenKey(userId));
}
public TokenModel parseToken(String token) {
TokenModel tokenModel=null;
Claims claims = jwtUtil.parseJWT(token);//解析获取token中的userId
if(claims!=null){
tokenModel=JSONUtils.fromJson(claims.getSubject(),TokenModel.class);
}
return tokenModel;
}
}
再来看我们的TokenController类了:用户登录获取token以及通过refreshToken获取token1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
"/api/v1/") (
public class TokenController {
private UserService userService;
private TokenService tokenService;
"用户登录", notes="该API用于用户登录,成功后返回用户信息") (value=
({
"username", value = "用户登录账号", required = true, dataType = "String"), (name =
"password", value = "用户登录密码", required = true, dataType = "String") (name =
})
"login",method = RequestMethod.POST) (value=
public ResponseEntity login(@RequestParam String username,@RequestParam String password){
Assert.notNull(username, "username can not be empty");
Assert.notNull(password, "password can not be empty");
UserModel userModel=userService.fetchUserByUserName(username);
if(userModel==null || !userModel.getPassword().equals(password)){
return ResponseUtil.custom("用户名或密码错误", Constant.USERNAME_OR_PASSWORD_ERROR);
}
//生成一个token,保存用户登录状态
String accessToken=tokenService.createAccessToken(userModel.getUserId());
String refreshToken=tokenService.createRefreshToken(userModel.getUserId());
if(accessToken==null||refreshToken==null){
return ResponseUtil.exception("创建token失败");
}
LoginResult loginResult=createResult(userModel);
loginResult.setAccess_token(accessToken);
loginResult.setExpire_in(Constant.JWT_TTL);
loginResult.setRefresh_token(refreshToken);
loginResult.setExpire_refresh_in(Constant.JWT_REFRESH_TTL);
return ResponseUtil.success("登录成功",loginResult);
}
"获取token", notes="该API用于用户使用refreshToken获取token") (value=
({
"grant_type", value = "授权类型", required = true, dataType = "String"), (name =
"refresh_token", value = "refresh_token", required = true, dataType = "String") (name =
})
"token",method = RequestMethod.POST) (value =
public ResponseEntity refresh(@RequestParam String grant_type,@RequestParam String refresh_token){
Assert.notNull(grant_type, "username can not be empty");
Assert.notNull(refresh_token, "password can not be empty");
if(!TextUtils.equals(grant_type,"refresh_token")){
return ResponseUtil.paramError();
}
if(!tokenService.checkRefreshToken(refresh_token)){//验证未通过
return ResponseUtil.custom(Constant.RESCODE_EXPIRE_OR_NOTEXIST,"refresh_token不正确或者已过期");
}
TokenModel tokenModel=tokenService.parseToken(refresh_token);
//生成一个token,保存用户登录状态
String accessToken=tokenService.createAccessToken(tokenModel.getUserId());
if(accessToken==null){
return ResponseUtil.exception("创建token失败");
}
RefreshResult refreshResult=new RefreshResult();
refreshResult.setAccess_token(accessToken);
refreshResult.setRefresh_token(refresh_token);
return ResponseUtil.success("获取token成功",refreshResult);
}
/**
* 创建返回结果
* @param userModel
* @return
*/
private LoginResult createResult(UserModel userModel){
LoginResult loginResult=new LoginResult();
loginResult.setUserId(userModel.getUserId());
loginResult.setGender(userModel.getGender());
loginResult.setMobile(userModel.getMobile());
loginResult.setEmail(userModel.getEmail());
loginResult.setNickName(userModel.getNickName());
loginResult.setAvatar(userModel.getAvatar());
loginResult.setBio(userModel.getBio());
loginResult.setBlog(userModel.getBlog());
return loginResult;
}
}
此外,我们创建一个注解Authorization,该注解用于标注哪些方法需要登录(即带有token信息)1
2
3
4
5 (ElementType.METHOD)
(RetentionPolicy.RUNTIME)
public Authorization {
}
最后要讲的是如何对于token的解析,创建一个AuthorizationInterceptor权限拦截器,在请求处理之前
我们拦截到token,对token的真实性及失效性进行分析
1 |
|
最后就是对刚刚创建的拦截器进行注册:1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class InterceptorConfiguration extends WebMvcConfigurerAdapter {
public AuthorizationInterceptor authorizationInterceptor(){
return new AuthorizationInterceptor();
}
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(authorizationInterceptor())
.addPathPatterns("/**");
}
}
写在最后
到此为止,我们已经创建了一个比较简易的能够支撑轻量级的APP服务端,然后根据我们的业务,在进行扩充就可以了。