RESTful登陆设计(基于Spring及Redis的Token鉴权)

转载:http://blog.csdn.net/gebitan505/article/details/51614805

 

什么是REST

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

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

使用Token进行身份鉴权

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

交互流程

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

程序示例

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

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

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

/**
 * 获取和删除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用于表示该操做须要登陆后才能进行:redis

  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状态码(未受权):spring

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

/**
 * 在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的流程进行操做。数据库

相关文章
相关标签/搜索