0
点赞
收藏
分享

微信扫一扫

基于Spring Boot 的RESTful权限认证

夏侯居坤叶叔尘 2021-09-30 阅读 22
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信息。

举报

相关推荐

0 条评论