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


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

     

    什么是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的流程进行操作。

  • 相关阅读:
    如何动态调用WebServices
    Cache及(HttpRuntime.Cache与HttpContext.Current.Cache)
    SQL创建索引(转)
    TSQL用法四:OpenDataSource, OpenRowSet
    AppDomain动态加载程序集
    hdu 2544 最短路
    hdu 1151 Air Raid
    hdu3790 最短路径问题
    hdu 1548 A strange lift
    对于 前K短路径问题 和 A*算法 的一些小小总结
  • 原文地址:https://www.cnblogs.com/ruiati/p/7060105.html
Copyright © 2020-2023  润新知