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

转载自博客cloud.tencent.com/developer/a...

视频地址参考详细讲解

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

收藏列表查询:

看请求头参数中是否携带正确的token,进行鉴权 鉴权成功获取用户信息,查询对应数据,鉴权失败,跳转到用户登录页面;

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

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

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

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

​ 过滤器是在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 自定义请求用户参数解析器

通过鉴权后:

判断 目标请求处理方法是否 自定义参数解析注解@RequestUser,且目标实体参数类型是User; 通过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配置类

手动创建请求token许可拦截器对象,放入容器 手动添加自定义拦截器到系统的拦截器组中; 手动创建自定义解析器对象,放入容器 手动添加自定义拦截器到系统的拦截器组中;

/** 复制代码
 * 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 ;
  2. 进行用户token自动鉴权;
  3. 在参数添加 自定义参数解析注解 @RequestUser
  4. 进行用户类型参数自动解析;(通过健全后,自动获取用户参数)
/** 复制代码
 * 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.2 用户token被删除

3.8 总结

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

相关推荐
Victor3562 小时前
Netty(20)如何实现基于Netty的WebSocket服务器?
后端
缘不易2 小时前
Springboot 整合JustAuth实现gitee授权登录
spring boot·后端·gitee
Kiri霧2 小时前
Range循环和切片
前端·后端·学习·golang
WizLC2 小时前
【Java】各种IO流知识详解
java·开发语言·后端·spring·intellij idea
Victor3562 小时前
Netty(19)Netty的性能优化手段有哪些?
后端
爬山算法2 小时前
Netty(15)Netty的线程模型是什么?它有哪些线程池类型?
java·后端
白宇横流学长3 小时前
基于SpringBoot实现的冬奥会科普平台设计与实现【源码+文档】
java·spring boot·后端
Python编程学习圈3 小时前
Asciinema - 终端日志记录神器,开发者的福音
后端
bing.shao3 小时前
Golang 高并发秒杀系统踩坑
开发语言·后端·golang
壹方秘境3 小时前
一款方便Java开发者在IDEA中抓包分析调试接口的插件
后端