SpringCloud(八) - 自定义token令牌,鉴权(注解+拦截器),参数解析(注解+解析器)

1、项目结构介绍

项目有使用到,redis和swagger,不在具体介绍;

2、手动鉴权和用户信息参数获取(繁杂,冗余)

2.1用户实体类

/**
 * Created On : 4/11/2022.
 * <p>
 * Author : huayu
 * <p>
 * Description: 用户实体
 */
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class User {

    //用户编号
    private String userId;

    //用户名
    private String  userName;

    //用户密码
    private String userPwd;

    //手机号
    private String userTel;

    //邮箱
    private String userEmail;

    //登录ip
    private String lastLoginIp;

}

2.2 业务层

2.2.1 接口

/**
 * Created On : 4/11/2022.
 * <p>
 * Author : huayu
 * <p>
 * Description: 用户业务接口
 */
public interface UserService {

    /**
     * @author : huayu
     * @date   : 4/11/2022
     * @param  : [userName, userPwd]
     * @return : java.lang.String
     * @description : 处理用户登录请求,校验用户信息是否正确,如果正确返回令牌
     */
    String userLogin(String userName,String userPwd);

    /**
     * @author : huayu
     * @date   : 5/11/2022
     * @param  : [userToken]
     * @return : void
     * @description : 用户登出
     */
    void userLogout(String userToken);

}

2.2.3 实现类

/**
 * Created On : 4/11/2022.
 * <p>
 * Author : huayu
 * <p>
 * Description: 用户业务接口实现类
 */
@Service
public class UserServiceImpl implements UserService {

    @Autowired
    private RedisUtils redisUtils;

    @Override
    public String userLogin(String userName, String userPwd) {

        //TODO 调用持久层接口,查询用户信息是否真确,如果查询到用户信息,说明用户存在,如果查询不到没说明用户不存在
        if("KH96".equals(userName) && "123456".equals(userPwd)){
            //代表用户的登录信息是正确的,可以生成token令牌,返回该客户端
            //令牌的生成规则,一般是随机串,长度不一,一般使用方式:可以选择UUID生成,或者将用户编号+其他信息进行md5加密,比如jwt
            String userToken = UUID.randomUUID().toString().replace("-", "");

            //简单模拟数据库查询出的用户详情
            User userLogin = User.builder()
                    .userId("T001")
                    .userName("KH96")
                    .userTel("13801020304")
                    .userEmail("kh97@kgc.com")
                    .lastLoginIp("127.0.0.1")
                    .build();

            //将查询的用户详情,直接一生成的token作为key,存入到redis缓存中,并增加时效(有过期时间,比如30分钟)
            redisUtils.set(userToken,userLogin,10*60);

            //返回有效的token令牌,此令牌就代表登录成功的用户
            return userToken;

        }

        //鉴权失败,返回null;
        return null;
    }

    @Override
    public void userLogout(String userToken) {
        // 直接将用户的token令牌长redis中删除
        if(redisUtils.hasKey(userToken)){
            redisUtils.del(userToken);
        }

    }

}

2.3 控制层

2.3.1 BaseController

/**
 * Created On : 4/11/2022.
 * <p>
 * Author : huayu
 * <p>
 * Description: 所有控制器的供父类,将所有有控制器要使用的公共方法,抽离到父类中,方便方法复用
 */
public class BaseController {

    /**
     * @author : huayu
     * @date   : 4/11/2022
     * @param  : [request, paramName]
     * @return : java.lang.String
     * @description : 从请求中获取参数,获取参数值,如果没有获取到,用空字符地带默认值的null
     */
    protected String getParameter(HttpServletRequest request,String paramName){
        return request.getParameter(paramName) == null ? "" : request.getParameter(paramName);
    }

    protected String getParameter(HttpServletRequest request,String paramName,String defaultValue){
        return request.getParameter(paramName) == null ? defaultValue : request.getParameter(paramName);
    }

    /**
     * @author : huayu
     * @date   : 4/11/2022
     * @param  : [request]
     * @return : java.lang.String
     * @description : getRemoteIp
     */
    protected String getRemoteIp(HttpServletRequest request) {
        // 获取ip
        String ip = request.getHeader("X-Real-IP");
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("x-forwarded-for");
        }
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("Proxy-Client-IP");
        }
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("WL-Proxy-Client-IP");
        }
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getRemoteAddr();
        }

        return ip;
    }


}

2.3.2 LoginController

用户登录:

  1. 根据用户名密码判断用户是否存在
  2. 存在生成token,返回给前端;不存在提示用户名或密码错误;
/**
 * Created On : 4/11/2022.
 * <p>
 * Author : huayu
 * <p>
 * Description: 用户登录登出
 */
@Slf4j
@RestController
@Api(tags = "用户登录登出类")
public class LoginController extends BaseController {

    @Autowired
    private UserService userService;

    @PostMapping("/login")
    @ApiOperation(value = "用户登录",notes = "支持token鉴权")
    @ApiImplicitParams({
            @ApiImplicitParam(value = "用户名",name = "userName",defaultValue = "KH96"),
            @ApiImplicitParam(value = "用户密码",name = "userPwd",defaultValue = "123456")
    })
    public RequestResult<String> doLogin(HttpServletRequest request){

        //获取请求中的用户名和密码参数
        String loginName = this.getParameter(request, "userName", "KH96");
        String loginPwd = this.getParameter(request, "userPwd", "123456");

        //调用业务接口,校验登录请求用户信息是否正确,如果正确,返回token令牌,否者返回null
        String userToken = userService.userLogin(loginName, loginPwd);

        //判断用户是否鉴权成功
        if(StringUtils.isNotBlank(userToken)){
            //登录鉴权成功,返回给客户端有限token令牌,前端保存,后续请求使用
            return  ResultBuildUtil.success(userToken);
        }

        return  ResultBuildUtil.fail("901","用户名或密码错误!");
    }


}

2.3.3 UserController

收藏列表查询:

  1. 看请求头参数中是否携带正确的token,进行鉴权
  2. 鉴权成功获取用户信息,查询对应数据,鉴权失败,跳转到用户登录页面;
/**
 * Created On : 4/11/2022.
 * <p>
 * Author : huayu
 * <p>
 * Description: 用户操作入口
 */
@Slf4j
@RestController
@Api(tags = "用户个人中心")
public class UserController {

    @Autowired
    private RedisUtils redisUtils;

    /**
     * @author : huayu
     * @date   : 4/11/2022
     * @param  : [request]
     * @return : com.kgc.scd.uitl.RequestResult<java.lang.String>
     * @description : 用户查询收藏列表,需要 token鉴权操作
     */
    @GetMapping("/collectionList")
    @ApiOperation(value = "收藏列表",notes = "支持token自动鉴权")
    public RequestResult<String> collectList(HttpServletRequest request){

        //直接获取前端请求的token参数进行鉴权操作,省略业务层接口操作
        String userToken = request.getHeader("token");

        //判断token是否合法,如果没有直接鉴权失败,跳转到登录
        if(StringUtils.isBlank(userToken)){
            //返回 鉴权失败
            return ResultBuildUtil.fail("902","token参数为空,请求失败,请求重新登录");
        }

        //判断token是否有效,如果redis中可以根据此token获取到信息,说明用户登录成功,且有效,否者鉴权失败,跳转到登录
        Object userObj = redisUtils.get(userToken);
        if(ObjectUtils.isEmpty(userObj)){
            //redis中没有该token的鉴权信息,饭后鉴权失败
            return ResultBuildUtil.fail("903","token 参数失效,请重新登录!");
        }

        //请求token值有效,直接将redis中存放的用户信息,转换为登录用户详情
        User loginUser = JSON.parseObject(userObj.toString(), User.class);

        //TODO 将鉴权通过的用户信息作为信息,调用查询用户收藏列表业务接口,获取该用户的收藏信息,返回给前端
        log.info("------  用户查看收藏列表,鉴权通过,当前登录用户:{}  ------",loginUser);

        //返货成功的收藏列表数据
        return ResultBuildUtil.success("查询用户收藏列表成功!\n "+loginUser);

    }

}

2.4 测试

2.4.1 测试用户登录

2.4.1.1 用户登陆成功

2.4.1.2 用户token添加成功

2.4.2 测试查询用户收藏信息

2.4.2.1 使用错误的token

2.4.2.2 使用正确的token

2.5 总结

虽然业务可以完成,但是每次都进行这样的手动鉴权和手动获取用户数据,比较繁琐,而且大量代码冗余;


3、自动鉴权和自动用户信息参数获取

3.1 原理

  • 自动鉴权
    • 自定义注解+自定义拦截器
  • 自动参数获取
    • 自定义注解+自定义解析器

3.2 自定义注解

3.2.1 自定义token鉴权注解

/**
 * Created On : 4/11/2022.
 * <p>
 * Author : huayu
 * <p>
 * Description: 请求token许可自定义注解,只要请求处理方法上加了此注解,就需要token鉴权
 */

@Target({ElementType.TYPE,ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RequestPermission {


}

3.2.2 自定义参数解析(获取)注解

/**
 * Created On : 4/11/2022.
 * <p>
 * Author : huayu
 * <p>
 * Description: 自定义请求用户注解,凡是在目标请求处理方法中,使用此注解,就自动解析redis中保存的登录用户,绑定到实体属性上
 */
@Target({ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RequestUser {
    
}

3.3 自定义请求token许可拦截器

  1. 判断目标请求方法是否需要鉴权,是返回true,否发false
    • 判断目标请求方法上是否有 添加了 请求token许可注解 @RequestPermission;
    • 判断目标请求类方法上是否 添加了 请求token许可注解 @RequestPermission;
  2. 鉴权
    • 鉴权成功不拦截;
    • 鉴权失败拦截;


回顾过滤器和拦截器的执行时机

​ 过滤器是在DispatcherServlet处理之前拦截,拦截器是在DispatcherServlet处理请求然后调用控制器方法(即我们自己写的处理请求的方法,用@RequestMapping标注)之前进行拦截。

/**
 * Created On : 4/11/2022.
 * <p>
 * Author : huayu
 * <p>
 * Description: 自定义请求token许可拦截器,拦截所有增加了请求token许可注解的请求,进行鉴权操作
 */
@Slf4j
public class TokenPermissionInterceptor implements HandlerInterceptor {

    @Autowired
    private RedisUtils redisUtils;

    @Override
    public boolean preHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object obj) throws Exception {

        // 判断是否需要校验请求token许可,只需要看目标请求处理方法上是否有自定义请求token许可注解-TokenPermission
        if (this.checkTargetMethodHasTokenPermission(obj)){
            // 需要进行请求token许可校验,从请求头中获取token参数,做token鉴权业务逻辑处理
            String userToken = httpServletRequest.getHeader("token");

            // 判断token是否合法,如果没有,直接鉴权失败,跳转到登录
            if(StringUtils.isBlank(userToken)){
                // token参数为空,返回鉴权失败
                this.returnTokenCheckJson(httpServletResponse, "902", "token参数为空,鉴权失败,请重新登录!");

                // 权限校验失败,需要拦截请求
                return false;
            }

            // 判断token是否有效,如果redis中可以根据此token值获取到信息,说明用户登录鉴权成功,且有效,否则鉴权失败,跳转到登录
            if(ObjectUtils.isEmpty(redisUtils.get(userToken))){
                // redis中没有该token的鉴权信息,返回鉴权失败
                this.returnTokenCheckJson(httpServletResponse, "903", "token参数失效,鉴权失败,请重新登录!");

                // 权限校验失败,需要拦截请求
                return false;
            }
        }

        // 不需要拦截,直接放行
        return true;
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object obj, ModelAndView modelAndView) throws Exception {

    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object obj, Exception ex) throws Exception {

    }

    /**
     * @author : zhukang
     * @date   : 2022/11/4
     * @param  : [handler]
     * @return : boolean
     * @description : 判断目标请求方法是否需要鉴权,是返回true,否发false
     */
    public boolean checkTargetMethodHasTokenPermission(Object handler){

        // 判断当前处理的handler是否已经映射到目标请求处理方法,看是不是HandlerMethod的实例对象
        if(handler instanceof HandlerMethod){
            // 强转为目标请求处理方法的实例对象,因为:HandlerMethod对象封装了目标请求处理方法的所有内容,包括方法所有的声明
            HandlerMethod handlerMethod = (HandlerMethod) handler;

            // 尝试获取目标请求处理方法上,是否添加了自定义请求token许可注解-TokenPermission,取到了就是加了,取不到就没加
            RequestPermission requestPermission = handlerMethod.getMethod().getAnnotation(RequestPermission.class);

            // 判断是否成功获取到请求token许可注解,如果没有获取到,不一定代表不需要进行权限校验,因为此注解还可能加载处理类,要再次尝试从请求处理方法所在处理类上获取该注解
            if(ObjectUtils.isEmpty(requestPermission)){
                requestPermission = handlerMethod.getMethod().getDeclaringClass().getAnnotation(RequestPermission.class);
            }

            // 最终判断是否需要进行请求token许可校验,如果获取到了,说明需要校验,否则直接放行
            return null != requestPermission;
        }

        // 请求不是需要进行鉴权操作,直接返回false
        return false;
    }



    /**
     * @author : zhukang
     * @date   : 2022/11/4
     * @param  : [response, returnCode, returnMsg]
     * @return : void
     * @description : 拦截器中,token鉴权失败的统一返回json处理
     */
    public void returnTokenCheckJson(HttpServletResponse response, String returnCode, String returnMsg){
        response.setCharacterEncoding("utf-8");
        response.setContentType("application/json; charset=utf-8");
        try {
            response.getWriter().print(JSON.toJSONString(ResultBuildUtil.fail(returnCode, returnMsg)));
        } catch (IOException e) {
            log.warn("****** 请求token许可拦截器返回结果异常:{} ******", e.getMessage());
        }
    }


}

3.4 自定义请求用户参数解析器

通过鉴权后:

  1. 判断 目标请求处理方法是否 自定义参数解析注解@RequestUser,且目标实体参数类型是User;
  2. 通过token为key取用redis中的用户信息;
/**
 * Created On : 4/11/2022.
 * <p>
 * Author : huayu
 * <p>
 * Description: 自定义请求用户参数解析器,自动根据 @RequestUser 注解,解析通过鉴权的用户信息,绑定到请求处理方法的用户参数上,要配合请求token许可鉴权使用
 */
public class MyDefineUserResolver implements HandlerMethodArgumentResolver {

    @Autowired
    private RedisUtils redisUtils;

    @Override
    public boolean supportsParameter(MethodParameter parameter) {
        // 决定是否需要执行参数解析,如果目标请求处理方法使用了自定义参数注解@RequestUser,且目标实体参数类型是User,就需要进行解析,否则不需要解析
        return parameter.hasParameterAnnotation(RequestUser.class) && parameter.getParameterType().isAssignableFrom(User.class);
    }

    @Override
    public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
        // 根据上面supportsParameter方法,如果返回的是true,代表需要执行方法参数解析,如果返回false,不需要执行参数解析
        // 从redis中获取token令牌保存的用户信息,转换为目标用户对象,绑定到请求处理方法的入参中,前提:鉴权是通过
        // TODO 在获取redis中保存的用户信息时,需要做非空校验,防止解析时过期
        return JSON.parseObject(redisUtils.get(webRequest.getHeader("token")).toString(), User.class);
    }
    
}

3.5 自定义webmvc配置类

  1. 手动创建请求token许可拦截器对象,放入容器
    • 手动添加自定义拦截器到系统的拦截器组中;
  2. 手动创建自定义解析器对象,放入容器
    • 手动添加自定义拦截器到系统的拦截器组中;
/**
 * Created On : 4/11/2022.
 * <p>
 * Author : huayu
 * <p>
 * Description: 自定义webmvc配置类,可以自定义
 */
@Configuration
public class MyDefineWebMVcConfig implements WebMvcConfigurer {

    /**
     * @author : huayu
     * @date   : 4/11/2022
     * @param  : []
     * @return : com.kgc.scd.interceptor.TokenPermissionInterceptor
     * @description : 手动创建请求token许可拦截器对象,放入容器,方便加入到系统拦截器组中
     */
    @Bean
    public TokenPermissionInterceptor tokenPermissionInterceptor(){
        return new TokenPermissionInterceptor();
    }

    /**
     * @author : huayu
     * @date   : 4/11/2022
     * @param  : []
     * @return : com.kgc.scd.resolver.MyDefineUserResolver
     * @description : 手动创建自定义解析器对象,放入容器,方便加入到系统解析器中
     */
    @Bean
    public MyDefineUserResolver myDefineUserResolver(){
       return  new MyDefineUserResolver();
    }

    @Override
    public void configurePathMatch(PathMatchConfigurer configurer) {

    }

    @Override
    public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {

    }

    @Override
    public void configureAsyncSupport(AsyncSupportConfigurer configurer) {

    }

    @Override
    public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {

    }

    @Override
    public void addFormatters(FormatterRegistry registry) {

    }

    @Override
    public void addInterceptors(InterceptorRegistry interceptorRegistry) {

        //手动添加自定义拦截器到系统的拦截器组中,才可以生效,否者不生效
        interceptorRegistry.addInterceptor(tokenPermissionInterceptor()).addPathPatterns("/**");

    }

    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {

    }

    @Override
    public void addCorsMappings(CorsRegistry registry) {

    }

    @Override
    public void addViewControllers(ViewControllerRegistry registry) {

    }

    @Override
    public void configureViewResolvers(ViewResolverRegistry registry) {

    }

    @Override
    public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
        //手动将容器中自定义请求用户解析器,加入到系统解析器中
        argumentResolvers.add(myDefineUserResolver());
    }

    @Override
    public void addReturnValueHandlers(List<HandlerMethodReturnValueHandler> returnValueHandlers) {

    }

    @Override
    public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {

    }

    @Override
    public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {

    }

    @Override
    public void configureHandlerExceptionResolvers(List<HandlerExceptionResolver> exceptionResolvers) {

    }

    @Override
    public void extendHandlerExceptionResolvers(List<HandlerExceptionResolver> exceptionResolvers) {

    }

    @Override
    public Validator getValidator() {
        return null;
    }

    @Override
    public MessageCodesResolver getMessageCodesResolver() {
        return null;
    }
}

3.5 UserController

  1. 在方法上或类上添加 自定义请求token许可注解 @RequestPermission ;
    • 进行用户token自动鉴权;
  2. 在参数添加 自定义参数解析注解 @RequestUser
    • 进行用户类型参数自动解析;(通过健全后,自动获取用户参数)
/**
 * Created On : 4/11/2022.
 * <p>
 * Author : huayu
 * <p>
 * Description: 用户操作入口
 */
@Slf4j
@RestController
@Api(tags = "用户个人中心")
@RequestPermission  //使用自定义请求token许可注解 当查看足迹列表时,需要进行token鉴权; 如果在类上增加了此注解,就地表当前类的所有处理方法都需要鉴权;
public class UserController {

    @Autowired
    private RedisUtils redisUtils;

    /**
     * @author : huayu
     * @date   : 4/11/2022
     * @param  : [request]
     * @return : com.kgc.scd.uitl.RequestResult<java.lang.String>
     * @description : 用户查询 足迹列表,需要 token鉴权操作
     */
    @GetMapping("/footList")
    @ApiOperation(value = "足迹列表",notes = "支持token自动鉴权")
    @RequestPermission  //使用自定义请求token许可注解 当查看足迹列表时,需要进行token鉴权
    public RequestResult<String> footList(@RequestUser @ApiIgnore User loginUser){

        //TODO 当遇到需要进行token鉴权操作,就必须重复上面的收藏鉴权操作,代码冗余,不利于扩展和维护
        //TODO 推荐用法:使用自定义实现自动鉴权,当添加了需要进行鉴权的自定义注解,执行鉴权操作,如果没添加则不需要

        //TODO 如果token鉴权成功,直接获取用户信息,调用业务接口,查询用户的足迹列表数据,返回前端

        log.info("------  用户查看足迹列表,鉴权通过,当前登录用户:{}  ------",loginUser);

        //返回成功的收藏列表数据
        return ResultBuildUtil.success("查询用户足迹列表成功!"+loginUser);

    }

}

3.6 LoginController 用户登出

/**
 * @author : huayu
 * @date   : 4/11/2022
 * @param  : [token]
 * @return : com.kgc.scd.uitl.RequestResult<java.lang.String>
 * @description : 用户退出登录
 */
@GetMapping("/logout")
@ApiOperation(value = "用户登出", notes = "用户删除token,退出系统")
public RequestResult<String> doLogout(@RequestHeader String token){

    // 调用业务接口,删除用户的token令牌
    userService.userLogout(token);

    return ResultBuildUtil.success("退出登录成功!");
}

3.7 测试

3.7.1 测试获取用户足迹

3.2.1.1 使用错误的token

3.2.1.2 使用正确的token

3.7.2 测试用户登出

3.7.2.1 用户登出成功

3.7.2.2 用户token被删除

3.8 总结

使用自定义鉴权注解 自动鉴权,和自定义参数解析注解 自动获取参数;代码量大大减少,而且操作方便;

posted @ 2022-12-01 19:51  hanease  阅读(682)  评论(0编辑  收藏  举报