什么是REST
REST(Representational State Transfer)是一种软件架构风格。它将服务端的信息和功能等所有事物统称为资源,客户端的请求实际就是对资源进行操作,它的主要特点有: – 每一个资源都会对应一个独一无二的url – 客户端通过HTTP的GET、POST、PUT、DELETE请求方法对资源进行查询、创建、修改、删除操作 – 客户端与服务端的交互必须是无状态的
关于RESTful的详细介绍可以参考这篇文章,在此就不浪费时间直接进入正题了。
使用Token进行身份鉴权
网站应用一般使用Session进行登陆用户信息的存储及验证,而在移动端使用Token则更加普遍。它们之间并没有太大区别,Token比较像是一个更加精简的自定义的Session。Session的主要功能是保持会话信息,而Token则只用于登录用户的身份鉴权。所以在移动端使用Token会比使用Session更加简易并且有更高的安全性,同时也更加符合RESTful中无状态的定义。
交互流程
- 客户端通过登陆请求提交用户名和密码,服务端验证通过后生成一个Token与该用户进行关联,并将Token返回给客户端。
- 客户端在接下来的请求中都会携带Token,服务端通过解析Token检查登录状态。
- 当用户退出登录、其他终端登录同一账号(被顶号)、长时间未进行操作时Token会失效,这时用户需要重新登陆。
程序示例
服务端生成的Token一般为随机的非重复字符串,根据应用对安全性的不同要求,会将其添加时间戳(通过时间判断Token是否被盗用)或url签名(通过请求地址判断Token是否被盗用)后加密进行传输。在本文中为了演示方便,仅是将User Id与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
|
/**
* Token的Model类,可以增加字段提高安全性,例如时间戳、url签名
* @author ScienJus
* @date 2015/7/31.
*/
public
class
TokenModel
{
//用户id
private
long
userId
;
//随机生成的uuid
private
String
token
;
public
TokenModel
(
long
userId
,
String
token
)
{
this
.
userId
=
userId
;
this
.
token
=
token
;
}
public
long
getUserId
(
)
{
return
userId
;
}
public
void
setUserId
(
long
userId
)
{
this
.
userId
=
userId
;
}
public
String
getToken
(
)
{
return
token
;
}
public
void
setToken
(
String
token
)
{
this
.
token
=
token
;
}
}
|
Redis是一个Key-Value结构的内存数据库,用它维护User Id和Token的映射表会比传统数据库速度更快,这里使用Spring-Data-Redis封装的TokenManager对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
|
/**
* 对token进行操作的接口
* @author ScienJus
* @date 2015/7/31.
*/
public
interface
TokenManager
{
/**
* 创建一个token关联上指定用户
* @param userId 指定用户的id
* @return 生成的token
*/
public
TokenModel
createToken
(
long
userId
)
;
/**
* 检查token是否有效
* @param model token
* @return 是否有效
*/
public
boolean
checkToken
(
TokenModel
model
)
;
/**
* 从字符串中解析token
* @param authentication 加密后的字符串
* @return
*/
public
TokenModel
getToken
(
String
authentication
)
;
/**
* 清除token
* @param userId 登录用户的id
*/
public
void
deleteToken
(
long
userId
)
;
}
/**
* 通过Redis存储和验证token的实现类
* @author ScienJus
* @date 2015/7/31.
*/
@
Component
public
class
RedisTokenManager
implements
TokenManager
{
private
RedisTemplate
<
Long
,
String
>
redis
;
@
Autowired
public
void
setRedis
(
RedisTemplate
redis
)
{
this
.
redis
=
redis
;
//泛型设置成Long后必须更改对应的序列化方案
redis
.
setKeySerializer
(
new
JdkSerializationRedisSerializer
(
)
)
;
}
public
TokenModel
createToken
(
long
userId
)
{
//使用uuid作为源token
String
token
=
UUID
.
randomUUID
(
)
.
toString
(
)
.
replace
(
"-"
,
""
)
;
TokenModel
model
=
new
TokenModel
(
userId
,
token
)
;
//存储到redis并设置过期时间
redis
.
boundValueOps
(
userId
)
.
set
(
token
,
Constants
.
TOKEN_EXPIRES_HOUR
,
TimeUnit
.
HOURS
)
;
return
model
;
}
public
TokenModel
getToken
(
String
authentication
)
{
if
(
authentication
==
null
||
authentication
.
length
(
)
==
0
)
{
return
null
;
}
String
[
]
param
=
authentication
.
split
(
"_"
)
;
if
(
param
.
length
!=
2
)
{
return
null
;
}
//使用userId和源token简单拼接成的token,可以增加加密措施
long
userId
=
Long
.
parseLong
(
param
[
0
]
)
;
String
token
=
param
[
1
]
;
return
new
TokenModel
(
userId
,
token
)
;
}
public
boolean
checkToken
(
TokenModel
model
)
{
if
(
model
==
null
)
{
return
false
;
}
String
token
=
redis
.
boundValueOps
(
model
.
getUserId
(
)
)
.
get
(
)
;
if
(
token
==
null
||
!
token
.
equals
(
model
.
getToken
(
)
)
)
{
return
false
;
}
//如果验证成功,说明此用户进行了一次有效操作,延长token的过期时间
redis
.
boundValueOps
(
model
.
getUserId
(
)
)
.
expire
(
Constants
.
TOKEN_EXPIRES_HOUR
,
TimeUnit
.
HOURS
)
;
return
true
;
}
public
void
deleteToken
(
long
userId
)
{
redis
.
delete
(
userId
)
;
}
}
|
RESTful中所有请求的本质都是对资源进行CRUD操作,所以登录和退出登录也可以抽象为对一个Token资源的创建和删除,根据该想法创建Controller:
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
|
/**
* 获取和删除token的请求地址,在Restful设计中其实就对应着登录和退出登录的资源映射
* @author ScienJus
* @date 2015/7/30.
*/
@
RestController
@
RequestMapping
(
"/tokens"
)
public
class
TokenController
{
@
Autowired
private
UserRepository
userRepository
;
@
Autowired
private
TokenManager
tokenManager
;
@
RequestMapping
(
method
=
RequestMethod
.
POST
)
public
ResponseEntity
<
ResultModel
>
login
(
@
RequestParam
String
username
,
@
RequestParam
String
password
)
{
Assert
.
notNull
(
username
,
"username can not be empty"
)
;
Assert
.
notNull
(
password
,
"password can not be empty"
)
;
User
user
=
userRepository
.
findByUsername
(
username
)
;
if
(
user
==
null
||
//未注册
!
user
.
getPassword
(
)
.
equals
(
password
)
)
{
//密码错误
//提示用户名或密码错误
return
new
ResponseEntity
<>
(
ResultModel
.
error
(
ResultStatus
.
USERNAME_OR_PASSWORD_ERROR
)
,
HttpStatus
.
NOT_FOUND
)
;
}
//生成一个token,保存用户登录状态
TokenModel
model
=
tokenManager
.
createToken
(
user
.
getId
(
)
)
;
return
new
ResponseEntity
<>
(
ResultModel
.
ok
(
model
)
,
HttpStatus
.
OK
)
;
}
@
RequestMapping
(
method
=
RequestMethod
.
DELETE
)
@
Authorization
public
ResponseEntity
<
ResultModel
>
logout
(
@
CurrentUser
User
user
)
{
tokenManager
.
deleteToken
(
user
.
getId
(
)
)
;
return
new
ResponseEntity
<>
(
ResultModel
.
ok
(
)
,
HttpStatus
.
OK
)
;
}
}
|
这个Controller中有两个自定义的注解分别是@Authorization
和@CurrentUser
,其中@Authorization
用于表示该操作需要登录后才能进行:
1
2
3
4
5
6
7
8
9
|
<
ol
class
=
"hibot"
>
<
li
rel
=
"0"
>
<
span
class
=
"comment"
>
/**</span> </li><li rel="1"><span class="comment"> * 在Controller的方法上使用此注解,该方法在映射时会检查用户是否登陆,未登录返回401错误</span> </li><li rel="1"><span class="comment"> * @author ScienJus</span> </li><li rel="1"><span class="comment"> * @date 2015/7/31.</span> </li><li rel="1"><span class="comment"> */
<
/
span
>
&
nbsp
;
<
/
li
>
<
li
rel
=
"0"
>
<
span
class
=
"annot"
>
@
Target
<
/
span
>
(
ElementType
.
METHOD
)
&
nbsp
;
<
/
li
>
<
li
rel
=
"0"
>
<
span
class
=
"annot"
>
@
Retention
<
/
span
>
(
RetentionPolicy
.
RUNTIME
)
&
nbsp
;
<
/
li
>
<
li
rel
=
"0"
>
<
span
class
=
"keyword"
>
public
<
/
span
>
&
nbsp
;
<
span
class
=
"annot"
>
@
interface
<
/
span
>
&
nbsp
;
Authorization
&
nbsp
;
{
&
nbsp
;
<
/
li
>
<
li
rel
=
"1"
>
}
<
/
li
>
<
/
ol
>
<
code
class
=
"lang-java"
style
=
"display: none;"
>
/**
* 在Controller的方法上使用此注解,该方法在映射时会检查用户是否登陆,未登录返回401错误
* @author ScienJus
* @date 2015/7/31.
*/
@
Target
(
ElementType
.
METHOD
)
@
Retention
(
RetentionPolicy
.
RUNTIME
)
public
@
interface
Authorization
{
}
|
这里使用Spring的拦截器完成这个功能,该拦截器会检查每一个请求映射的方法是否有@Authorization
注解,并使用TokenManager验证Token,如果验证失败直接返回401状态码(未授权):
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
|
/**
* 自定义拦截器,判断此次请求是否有权限
* @author ScienJus
* @date 2015/7/30.
*/
@
Component
public
class
AuthorizationInterceptor
extends
HandlerInterceptorAdapter
{
@
Autowired
private
TokenManager
manager
;
public
boolean
preHandle
(
HttpServletRequest
request
,
HttpServletResponse
response
,
Object
handler
)
throws
Exception
{
//如果不是映射到方法直接通过
if
(
!
(
handler
instanceof
HandlerMethod
)
)
{
return
true
;
}
HandlerMethod
handlerMethod
=
(
HandlerMethod
)
handler
;
Method
method
=
handlerMethod
.
getMethod
(
)
;
//从header中得到token
String
authorization
=
request
.
getHeader
(
Constants
.
AUTHORIZATION
)
;
//验证token
TokenModel
model
=
manager
.
getToken
(
authorization
)
;
if
(
manager
.
checkToken
(
model
)
)
{
//如果token验证成功,将token对应的用户id存在request中,便于之后注入
request
.
setAttribute
(
Constants
.
CURRENT_USER_ID
,
model
.
getUserId
(
)
)
;
return
true
;
}
//如果验证token失败,并且方法注明了Authorization,返回401错误
if
(
method
.
getAnnotation
(
Authorization
.
class
)
!=
null
)
{
response
.
setStatus
(
HttpServletResponse
.
SC_UNAUTHORIZED
)
;
return
false
;
}
return
true
;
}
}
|
@CurrentUser
注解定义在方法的参数中,表示该参数是登录用户对象。这里同样使用了Spring的解析器完成参数注入:
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
|
/**
* 在Controller的方法参数中使用此注解,该方法在映射时会注入当前登录的User对象
* @author ScienJus
* @date 2015/7/31.
*/
@
Target
(
ElementType
.
PARAMETER
)
@
Retention
(
RetentionPolicy
.
RUNTIME
)
public
@
interface
CurrentUser
{
}
/**
* 增加方法注入,将含有CurrentUser注解的方法参数注入当前登录用户
* @author ScienJus
* @date 2015/7/31.
*/
@
Component
public
class
CurrentUserMethodArgumentResolver
implements
HandlerMethodArgumentResolver
{
@
Autowired
private
UserRepository
userRepository
;
@
Override
public
boolean
supportsParameter
(
MethodParameter
parameter
)
{
//如果参数类型是User并且有CurrentUser注解则支持
if
(
parameter
.
getParameterType
(
)
.
isAssignableFrom
(
User
.
class
)
&
amp
;
&
amp
;
parameter
.
hasParameterAnnotation
(
CurrentUser
.
class
)
)
{
return
true
;
}
return
false
;
}
@
Override
public
Object
resolveArgument
(
MethodParameter
parameter
,
ModelAndViewContainer
mavContainer
,
NativeWebRequest
webRequest
,
WebDataBinderFactory
binderFactory
)
throws
Exception
{
//取出鉴权时存入的登录用户Id
Long
currentUserId
=
(
Long
)
webRequest
.
getAttribute
(
Constants
.
CURRENT_USER_ID
,
RequestAttributes
.
SCOPE_REQUEST
)
;
if
(
currentUserId
!=
null
)
{
//从数据库中查询并返回
return
userRepository
.
findOne
(
currentUserId
)
;
}
throw
new
MissingServletRequestPartException
(
Constants
.
CURRENT_USER_ID
)
;
}
}
|
一些细节
- 登陆请求一定要使用HTTPS,否则无论Token做的安全性多好密码泄露了也是白搭
- Token的生成方式有很多种,例如比较热门的有JWT(JSON Web Tokens)、OAuth等。
源码发布
本文的完整示例程序已发布在我的Github上,可以下载并按照readme.md的流程进行操作。
转载:http://www.scienjus.com/restful-token-authorization/