服务端直接使用了客户端传过来的用户标识,导致了安全问题:
@GetMapping("wrong")
public String wrong(@RequestParam("userId") Long userId) {
return "当前用户Id:" + userId;
}
开发同学没有正确认识接口或服务面向的用户。如果接口面向内部服务,由服务调用方传入用户 ID 没什么不合理,但是这样的接口不能直接开放给客户 端或 H5 使用。在测试阶段为了方便测试调试,我们通常会实现一些无需登录即可使用的接口,直接使用客户端传过来的用户标识,却在上线之前忘记删 除类似的超级接口。一个大型网站前端可能由不同的模块构成,不一定是一个系统,而用户登录状态可能也没有打通。有些时候,我们图简单可能会在 URL 中直接传用户 ID,以实现通过前端传值来打通用户登录状态。如果你的接口直面用户(比如给客户端或 H5 页面调用),那么一定需要用户先登录才 能使用。登录后用户标识保存在服务端,接口需要从服务端(比如 Session 中)获取。这里有段代码演示了一个最简单的登录操作,登录后在 Session 中设 置了当前用户的标识。
@GetMapping("login")
public long login(@RequestParam("username") String username, @RequestParam("password") String password, HttpSession session) {
public long login(@RequestParam("username") String username, @RequestParam("password")String password, HttpSession session) {
if (username.equals("admin") && password.equals("admin")) {
session.setAttribute("currentUser", 1L);
return 1L;
}
return 0L;
}
关于登陆的逻辑,其实我们没有必要在每一个方法内都复制粘贴相同的获取用户身份的逻辑,可以定义一个自定义注解 @LoginRequired 到 userId 参数上, 然后通过 HandlerMethodArgumentResolver 自动实现参数的组装。
@GetMapping("right")
public String right(@LoginRequired Long userId) {
return "当前用户Id:" + userId;
}
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.PARAMETER)
@Documented
public @interface LoginRequired {
String sessionKey() default "currentUser";
}
@Slf4j
public class LoginRequiredArgumentResolver implements HandlerMethodArgumentResolver {
//解析哪些参数
@Override
public boolean supportsParameter(MethodParameter methodParameter) {
//匹配参数上具有@LoginRequired注解的参数
return methodParameter.hasParameterAnnotation(LoginRequired.class);
}
@Override
public Object resolveArgument(MethodParameter methodParameter, ModelAndViewContainer modelAndViewContainer, NativeWebRequest nativeWebRequest,
WebDataBinderFactory webDataBinderFactory) throws Exception {
//从参数上获得注解
LoginRequired loginRequired = methodParameter.getParameterAnnotation(LoginRequired.class);
//根据注解中的Session Key,从Session中查询用户信息
Object object = nativeWebRequest.getAttribute(loginRequired.sessionKey(), NativeWebRequest.SCOPE_SESSION);
if (object == null) {
log.error("接口 {} 非法调用!", methodParameter.getMethod().toString());
throw new RuntimeException("请先登录!");
}
return object;
}
}
当然,我们要实现 WebMvcConfigurer 接口的 addArgumentResolvers 方法,来增加这个自定义的处理器 LoginRequiredArgumentResolver。?
SpringBootApplication
public class CommonMistakesApplication implements WebMvcConfigurer {
...
@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
resolvers.add(new LoginRequiredArgumentResolver());
}
}