美文网首页Spring Boot程序员Spring Boot
基于Spring Boot 的RESTful权限认证

基于Spring Boot 的RESTful权限认证

作者: 王不哈 | 来源:发表于2018-01-14 10:57 被阅读789次
    1. 使用Token进行身份鉴权

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

    2. 交互流程
    • 客户端通过登陆接口提交用户名密码
    • 服务端检查后生成token,并与用户关联(这里使用redis缓存起来)
    • 客户端在之后的请求都携带token,服务端通过token检查用户身份
    3. 实现
    • Token实体
    public class TokenEntity {
        private String userId;
        private String token;
        //忽略构造方法和setter/getter
    }
    
    • User实体
    @Entity
    @Table(name = "user")
    public class UserEntity {
        @Id
        @GeneratedValue(generator = "system-uuid")
        @GenericGenerator(name = "system-uuid", strategy = "uuid")
        @Column(name = "user_id", unique = true, nullable = false, length = 32)
        private String userId;
    
        @JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")
        private Date createDate;
    
        private String userName;
        private String password;
        private boolean dr;
        //忽略构造方法和setter/getter
    
    • 用于管理Token的服务接口
    public interface TokenService {
        /**
         * 创建一个token关联上指定用户
         *
         * @param userId 指定用户的id
         * @return 生成的token
         */
        TokenEntity createToken(String userId);
    
        /**
         * 检查token是否有效
         *
         * @param model token
         * @return 是否有效
         */
        boolean checkToken(TokenEntity model);
    
        /**
         * 从字符串中解析token
         *
         * @param authentication 加密后的字符串
         * @return Token实例
         */
        TokenEntity getToken(String authentication);
    
        /**
         * 清除token
         *
         * @param userId 登录用户的id
         */
        void deleteToken(String userId);
    }
    
    • 使用Reids管理Token的TokenService 实现类
    package com.sukaiyi.demo.certification.service.impl;
    
    import com.sukaiyi.demo.certification.constants.Constants;
    import com.sukaiyi.demo.certification.entity.TokenEntity;
    import com.sukaiyi.demo.certification.service.TokenService;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.data.redis.core.RedisTemplate;
    import org.springframework.stereotype.Component;
    import org.springframework.util.StringUtils;
    
    import java.util.UUID;
    import java.util.concurrent.TimeUnit;
    
    /**
     * @author sukaiyi
     */
    @Component
    public class RedisTokenService implements TokenService {
    
        @Autowired
        RedisTemplate<String, TokenEntity> redisTemplate;
    
        @Override
        public TokenEntity createToken(String userId) {
            String token = UUID.randomUUID().toString();
            TokenEntity tokenEntity = new TokenEntity(userId, token);
            redisTemplate.opsForValue().set(userId, tokenEntity, Constants.TOKEN_EXPIRES_HOUR, TimeUnit.MINUTES);
            return tokenEntity;
        }
    
        @Override
        public boolean checkToken(TokenEntity entity) {
            if (entity == null) {
                return false;
            }
            TokenEntity token = redisTemplate.opsForValue().get(entity.getUserId());
            if (token == null || StringUtils.isEmpty(token.getToken())) {
                return false;
            }
            return token.getToken().equals(entity.getToken());
        }
    
        @Override
        public TokenEntity getToken(String authentication) {
            //      userId 为32位字符串
            //      userId拼接token得到authentication
            //      所以要求authentication长度大于32
            if (!StringUtils.isEmpty(authentication) && authentication.length() > 32) {
                TokenEntity tokenEntity = new TokenEntity();
                String userId = authentication.substring(0, 32);
                String token = authentication.substring(32);
                tokenEntity.setUserId(userId);
                tokenEntity.setToken(token);
                return tokenEntity;
            }
            return null;
        }
    
        @Override
        public void deleteToken(String userId) {
            redisTemplate.delete(userId);
        }
    }
    
    • 登陆/注销的RESTful接口
    @RestController
    @RequestMapping("/tokens")
    public class TokenController {
        @Autowired
        private UserRepository userRepository;
    
        @Autowired
        private TokenService tokenService;
    
        @RequestMapping(method = RequestMethod.POST)
        public ResponseEntity login(@RequestParam String userName, @RequestParam String password) throws BusinessException {
            if (StringUtils.isEmpty(userName) || StringUtils.isEmpty(password)) {
                throw new BusinessException(ExceptionCode.NEED_FIELD, "用户名或密码为空");
            }
            UserEntity user = userRepository.findUserByUserName(userName);
            if (user == null) {
                throw new BusinessException(ExceptionCode.NOT_FOUND, "用户不存在");
            }
            if (MD5Util.encode(password).equals(user.getPassword())) {
                TokenEntity tokenEntity = tokenService.createToken(user.getUserId());
                return new ResponseEntity<>(ResultEntity.ok(tokenEntity.getUserId() + tokenEntity.getToken()), HttpStatus.OK);
            }
            throw new BusinessException(ExceptionCode.AUTHORIZATION_FAILED, "用户名或密码错误");
        }
    
        @Authorization
        @RequestMapping(method = RequestMethod.DELETE)
        public ResponseEntity logout(@CurrentUser UserEntity user) {
            tokenService.deleteToken(user.getUserId());
            return new ResponseEntity<>(ResultEntity.ok("注销成功"), HttpStatus.OK);
        }
    
    }
    

    其中logout方法的有关的两个注解@Authorization@CurrentUser:
    @Authorization标注在controller的rest方法上,表示访问这个资源需要登陆授权;

    /**
     * 在Controller的方法上使用此注解,该方法在映射时会检查用户是否登录,未登录返回401错误
     * @author sukaiyi
     */
    @Target(ElementType.METHOD)
    @Retention(RetentionPolicy.RUNTIME)
    public @interface Authorization {
    }
    

    @CurrentUser标注在方法的参数上,用于自动将当前登陆用户注入到该参数。

    /**
     * 在Controller的方法参数中使用此注解,该方法在映射时会注入当前登录的User对象
     *
     * @author sukaiyi
     */
    @Target(ElementType.PARAMETER)
    @Retention(RetentionPolicy.RUNTIME)
    public @interface CurrentUser {
    }
    
    • 处理Authorization注解的拦截器
    @Component
    public class AuthorizationInterceptor extends HandlerInterceptorAdapter {
    
        @Autowired
        private TokenService tokenService;
    
        public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
            if (!(handler instanceof HandlerMethod)) {
                return true;
            }
            HandlerMethod handlerMethod = (HandlerMethod) handler;
            Method method = handlerMethod.getMethod();
            if (method.getAnnotation(Authorization.class) == null){
                return true;
            }
            String authorization = request.getHeader(Constants.AUTHORIZATION);
            TokenEntity tokenEntity = tokenService.getToken(authorization);
    
            if (tokenService.checkToken(tokenEntity)) {
                //如果token验证成功,将token对应的用户id存在request中,便于之后注入
                request.setAttribute(Constants.CURRENT_USER_ID, tokenEntity.getUserId());
                return true;
            }else{
                response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
                return false;
            }
        }
    }
    

    拦截器配置为Bean,并添加拦截器

    @SpringBootApplication
    public class DemoApplication extends WebMvcConfigurerAdapter {
    
        public static void main(String[] args) {
            SpringApplication.run(DemoApplication.class, args);
        }
    
        @Bean
        AuthorizationInterceptor authorizationInterceptor() {
            return new AuthorizationInterceptor();
        }
    
        @Override
        public void addInterceptors(InterceptorRegistry registry) {
            // 多个拦截器组成一个拦截器链
            // addPathPatterns 用于添加拦截规则
            // excludePathPatterns 用于排除拦截
            registry.addInterceptor(authorizationInterceptor()).addPathPatterns("/**");
            super.addInterceptors(registry);
        }
    }
    
    • 处理CurrentUser注解的参数解析器
    /**
     * @author sukaiyi
     */
    public class CurrentUserArgumentResolver implements HandlerMethodArgumentResolver {
    
        @Autowired
        private UserService userService;
    
        @Override
        public boolean supportsParameter(MethodParameter methodParameter) {
            //如果参数类型是UserEntity并且有CurrentUser注解则支持
            return methodParameter.getParameterType().isAssignableFrom(UserEntity.class) &&
                    methodParameter.hasParameterAnnotation(CurrentUser.class);
        }
    
        @Override
        public Object resolveArgument(MethodParameter parameter,
                                      ModelAndViewContainer container,
                                      NativeWebRequest request,
                                      WebDataBinderFactory factory) throws BusinessException {
            //取出AuthorizationInterceptor中注入的userId
            String currentUserId = (String) request.getAttribute(Constants.CURRENT_USER_ID, RequestAttributes.SCOPE_REQUEST);
            if (currentUserId != null) {
                return userService.findByUserId(currentUserId);
            }
            throw new BusinessException(ExceptionCode.NOT_FOUND, "用户不存在");
        }
    }
    

    参数解析器配置为Bean,并添加

    @SpringBootApplication
    public class DemoApplication extends WebMvcConfigurerAdapter {
    
        public static void main(String[] args) {
            SpringApplication.run(DemoApplication.class, args);
        }
    
        @Bean
        CurrentUserArgumentResolver currentUserArgumentResolver() {
            return new CurrentUserArgumentResolver();
        }
    
        @Override
        public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
            argumentResolvers.add(currentUserArgumentResolver());
            super.addArgumentResolvers(argumentResolvers);
        }
    }
    
    4. 总结

    此时客户端调用Authorization注解修饰的REST接口时,需要在请求头中携带authorization信息,否则将返回401错误,该信息为登陆时服务端返回的token信息。

    相关文章

      网友评论

        本文标题:基于Spring Boot 的RESTful权限认证

        本文链接:https://www.haomeiwen.com/subject/ysohoxtx.html