转载自:http://www.scienjus.com/restful-token-authorization/

http://m.blog.csdn.net/article/details?id=49903715

什么是REST

REST(Representational State Transfer)是一种软件架构风格。它将服务端的信息和功能等所有事物统称为资源,客户端的请求实际就是对资源进行操作,它的主要特点有: – 每一个资源都会对应一个独一无二的url – 客户端通过HTTP的GET、POST、PUT、DELETE请求方法对资源进行查询、创建、修改、删除操作 – 客户端与服务端的交互必须是无状态的

关于RESTful的详细介绍可以参考这篇文章,在此就不浪费时间直接进入正题了。

使用Token进行身份鉴权

网站应用一般使用Session进行登录用户信息的存储及验证,而在移动端使用Token则更加普遍。它们之间并没有太大区别,Token比较像是 一个更加精简的自定义的Session。Session的主要功能是保持会话信息,而Token则只用于登录用户的身份鉴权。所以在移动端使用Token 会比使用Session更加简易并且有更高的安全性,同时也更加符合RESTful中无状态的定义。

交互流程

  1. 客户端通过登录请求提交用户名和密码,服务端验证通过后生成一个Token与该用户进行关联,并将Token返回给客户端。
  2. 客户端在接下来的请求中都会携带Token,服务端通过解析Token检查登录状态。
  3. 当用户退出登录、其他终端登录同一账号(被顶号)、长时间未进行操作时Token会失效,这时用户需要重新登录。

程序示例

服务端生成的Token一般为随机的非重复字符串,根据应用对安全性的不同要求,会将其添加时间戳(通过时间判断Token是否被盗用)或url签 名(通过请求地址判断Token是否被盗用)后加密进行传输。在本文中为了演示方便,仅是将User Id与Token以”_”进行拼接。

/**
* 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进行基础操作:

/**
* 对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 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:

/**
* 获取和删除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 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 logout(@CurrentUser User user) {
tokenManager.deleteToken(user.getId());
return new ResponseEntity<>(ResultModel.ok(), HttpStatus.OK);
} }

这个Controller中有两个自定义的注解分别是@Authorization@CurrentUser,其中@Authorization用于表示该操作需要登录后才能进行:

  1. /**
  2. * 在Controller的方法上使用此注解,该方法在映射时会检查用户是否登录,未登录返回401错误
  3. * @author ScienJus
  4. * @date 2015/7/31.
  5. */
  6. @Target(ElementType.METHOD)
  7. @Retention(RetentionPolicy.RUNTIME)
  8. public @interface Authorization {
  9. }

这里使用Spring的拦截器完成这个功能,该拦截器会检查每一个请求映射的方法是否有@Authorization注解,并使用TokenManager验证Token,如果验证失败直接返回401状态码(未授权):

/**
* 自定义拦截器,判断此次请求是否有权限
* @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的解析器完成参数注入:

/**
* 在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) &&
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的流程进行操作。

RESTful登录设计(基于Spring及Redis的Token鉴权)的更多相关文章

  1. # RESTful登录(基于token鉴权)的设计实例

    使用场景 现在很多基于restful的api接口都有个登录的设计,也就是在发起正式的请求之前先通过一个登录的请求接口,申请一个叫做token的东西.申请成功后,后面其他的支付请求都要带上这个token ...

  2. 全栈项目|小书架|微信小程序-登录及token鉴权

    小程序登录 之前也写过微信小程序登录的相关文章: 微信小程序~新版授权用户登录例子 微信小程序-携带Token无感知登录的网络请求方案 微信小程序开通云开发并利用云函数获取Openid 也可以通过官方 ...

  3. 单点登录实现(spring session+redis完成session共享)

    一.前言 项目中用到的SSO,使用开源框架cas做的.简单的了解了一下cas,并学习了一下 单点登录的原理,有兴趣的同学也可以学习一下,写个demo玩一玩. 二.工程结构 我模拟了 sso的客户端和s ...

  4. 关于防范csrf攻击基于token鉴权

    在web开发中,之前都使用cookie + session方式来实现身份认证鉴权.但是现在前后端分离,以及终端有可能不支持cookie的情况下,一般都采用token方式.现在系统设计思路如下: 服务端 ...

  5. shiro开发,shiro的环境配置(基于spring+springMVC+redis)

    特别感谢lhacker分享的文章,对我帮助很大 http://www.aiuxian.com/article/p-1913280.html 基本的知识就不在这里讲了,在实战中体会shiro的整体设计理 ...

  6. Spring Cloud服务间调用鉴权

    学习使用Spring Cloud 微服务间的调用都是RestFul风格,如何保证调用之间的安全性,这是一个很重要的问题. 通过查阅资料http://wiselyman.iteye.com/blog/2 ...

  7. Spring Cloud实战: 基于Spring Cloud Gateway + vue-element-admin 实现的RBAC权限管理系统,实现网关对RESTful接口方法权限和自定义Vue指令对按钮权限的细粒度控制

    一. 前言 信我的哈,明天过年. 这应该是农历年前的关于开源项目 的最后一篇文章了. 有来商城 是基于 Spring Cloud OAuth2 + Spring Cloud Gateway + JWT ...

  8. Spring集成Redis方案(spring-data-redis)(基于Jedis的单机模式)(待实践)

    说明:请注意Spring Data Redis的版本以及Spring的版本!最新版本的Spring Data Redis已经去除Jedis的依赖包,需要自行引入,这个是个坑点.并且会与一些低版本的Sp ...

  9. Spring Data Redis 让 NoSQL 快如闪电 (1)

    [编者按]本文作者为 Xinyu Liu,详细介绍了 Redis 的特性,并辅之以丰富的用例.在本文的第一部分,将重点概述 Redis 的方方面面.文章系国内 ITOM 管理平台 OneAPM 编译呈 ...

随机推荐

  1. JAVA标签的使用跳出循环

    public static void main(String args[]) { int i=10,j=10; outer: while (i > 0) { inner: while (j &g ...

  2. c语言参数类型

    今天看ntcip源码时看到,函数参数有点不一样.在函数实现时,没有括号中没有指明参数类型.注意这里说的是函数实现,不是说函数声明.这里在函数列表括号后面做了类型的说明,以前看到过,没想起来,今天做个记 ...

  3. 团队开发之《极速蜗牛》NABC分析

    一.简介 项目名称:极速蜗牛 特点:操作简单,视觉与听觉配合,让用户有最完美的体验. 二.NABC分析 N(need):在人们无时无刻离不开手机的今天,难免有无聊的时候,此刻一款操作简单又能令人们动脑 ...

  4. github实践操作

    一.本地仓库的创建和提交 1.下载并安装Git http://msysgit.github.io/,安装完成后在本地电脑创建一个git仓库并初始化本地仓库 2.在git目录下创建一个Readme.tx ...

  5. 随笔 planetest

    Camera跟随物体: import Scripts包,Component中的camera control会有smooth follow脚本,添加到Main Camera中,在脚本的target属性中 ...

  6. 如何将后台传来的json反序列化为前端具体对象

    //jQuery方式 var obj = $.parseJSON(json); ....   //eval var obj = eval("("+json+")" ...

  7. springmvc返回jsp源代码解决办法

    url-pattern问题 spring用到forward("/WEB-INF/jsp/*.jsp")而forward当然是又要经过web.xml的映射的,然后,在URL匹配时,  ...

  8. SwipeBackLayout的使用方法,右滑返回

    使用方法: 需要右滑返回的activity继承baseActivity就可以, 如: public class SettingActivity extends BaseActivity {} 为防止滑 ...

  9. 你自认为理解了JavaScript?

    关于Dmitry Baranovskiy 的博客中一篇文章(http://dmitry.baranovskiy.com/post/91403200),其中有五段小代码,用来测试是否理解 JavaScr ...

  10. HDU 1532 Drainage Ditches 分类: Brush Mode 2014-07-31 10:38 82人阅读 评论(0) 收藏

    Drainage Ditches Time Limit: 2000/1000 MS (Java/Others)    Memory Limit: 65536/32768 K (Java/Others) ...