超详细! springboot整合Filter_拦截器_注解+aop的方式实现token校验

分类:

  • Java Web中提供的Filter
  • SpringMvc中提供的拦截器Interceptor
  • Spring提供的AOP技术+自定义注解

下边结合jwt、redis实现简单的实现
这里redis整合的具体教程就不详细讲了,具体可以查看该文章
【redis系列】springboot整合redis
首先先导入依赖

        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
            <version>0.9.1</version>
        </dependency>

filter方式

这种方式目前是不大推荐的,因为它需要手动设置需要进行拦截的接口
image.png

1、jwt参数类、配置

这个的目的是为了将参数封装在类里面,不需要每次都是使用@Value去获取。

package com.walker.dianping.common.properties;

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

@Data
@Component
@ConfigurationProperties(prefix = "jwt")
public class JWTProperties {
    private String secret;
    private long expire;
    private String header;
    private String whiteList;
}

application.yml

jwt:
  # 加密密钥
  secret: safr3414fffdw1
  # token有效时长 单位秒
  expire: 3600
  # header 名称
  header: Authorization
  # 白名单
  whiteList: /login
2、编写过滤器 TokenFilter

首先需要实现Filter,然后去重写他的方法
过滤方法的逻辑大致如下:
image.png

package com.walker.dianping.common.config.interceptor;

import cn.hutool.core.util.StrUtil;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.walker.dianping.common.constants.SystemConstant;
import com.walker.dianping.common.properties.JWTProperties;
import com.walker.dianping.common.utils.Assert;
import com.walker.dianping.entity.R;
import com.walker.dianping.entity.TbUserEntity;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;

import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.TimeUnit;


@Slf4j
//注册到容器中
@Component
//1、实现Filter,然后重写init,doFilter,destroy方法
public class TokenFilter implements Filter {

    @Autowired
    private JWTProperties jwtProperties;

    //redisTemplate redis的工具类
    @Autowired
    private StringRedisTemplate redisTemplate;
    

    String WHILE_LIST="/login";
    String TOKEN_USER_KEY="token:user:";


    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        Filter.super.init(filterConfig);
    }


    
    //过滤方法
    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        //先将其转成HttpServletRequest
        HttpServletRequest request=(HttpServletRequest) servletRequest;

        String requestURI = request.getRequestURI();
        //如果不在白名单中,则检测token是否正常
        if(!WHILE_LIST.contains(requestURI)){	

            //获取请求头的参数 
            String token = request.getHeader(jwtProperties.getHeader());
            if(StrUtil.isEmpty(token)){
                resp(servletResponse,"账号未登录");
                return;
            }
            
            //从redis中获取token是否存在,是否过期
            String json = redisTemplate.opsForValue().get(TOKEN_USER_KEY + token);
            if(StrUtil.isEmpty(json)){
                resp(servletResponse,"账号未登录");
                return;
            }

        }

        filterChain.doFilter(servletRequest, servletResponse);
    }



    @Override
    public void destroy() {
        Filter.super.destroy();
    }


    //相应方法封装
        private void resp(ServletResponse servletResponse,String msg) throws IOException {
        HttpServletResponse response=(HttpServletResponse) servletResponse;
        ServletOutputStream outputStream = response.getOutputStream();
        response.setStatus(SystemConstant.CODE_401);
        response.setContentType(SystemConstant.JSON);
        R<Object> r = R.fail(msg);
        outputStream.write(JSON.toJSONString(r).getBytes(StandardCharsets.UTF_8));
    }
}

3、WebMvcConfig 实现WebMvcConfigurer
package com.walker.dianping.common.config;

import com.walker.dianping.common.config.interceptor.TokenFilter;
import com.walker.dianping.common.properties.JWTProperties;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

import javax.servlet.Filter;

@Configuration
    //实现WebMvcConfigurer
public class WebMvcConfig implements WebMvcConfigurer {

    @Autowired
    private TokenFilter tokenFilter;


    /**
    * 登录过滤器
	* 如果这个没有配置的话,默认所有的请求都会走filter
    */
    @Bean
    public FilterRegistrationBean<Filter> loginFilterRegistration(){
        FilterRegistrationBean<Filter> registrationBean = new FilterRegistrationBean<>();
        //设置过滤器
        registrationBean.setFilter(tokenFilter);
        registrationBean.setName("loginFilter");
        //拦截路径,这个就不大好,每次新增接口都得添加新的拦截器
        registrationBean.addUrlPatterns("/test/get");
        //指定顺序,数字越小越靠前
        registrationBean.setOrder(-1);
        return registrationBean;
    }



}

4、编写测试接口
package com.walker.dianping.controller;

import com.walker.dianping.entity.R;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/test")
public class TestController {

    @GetMapping("/get")
    public R get(){
        return R.ok("hello");
    }
}

  • 请求头未带token时

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xFP0pqwq-1675924860207)(https://cdn.nlark.com/yuque/0/2023/png/21370279/1675331842944-17baa622-b569-4b4c-8a98-0775808033ae.png#averageHue=%23fcfbfb&clientId=u2771c5eb-b1d5-4&from=paste&height=535&id=uf2bdddf8&name=image.png&originHeight=669&originWidth=1457&originalType=binary&ratio=1&rotation=0&showTitle=false&size=59731&status=done&style=none&taskId=u888d20f1-82df-4dac-a351-1755efd1f0b&title=&width=1165.6)]

  • 请求头带token时

image.png

HandlerInterceptor 拦截器

这个接口是springboot自带的,实现起来方便了不少

1、编写token拦截器
package com.walker.dianping.common.config.interceptor;

import cn.hutool.core.util.StrUtil;
import com.alibaba.fastjson.JSON;
import com.walker.dianping.common.constants.SystemConstant;
import com.walker.dianping.common.properties.JWTProperties;
import com.walker.dianping.entity.R;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;

import javax.servlet.ServletOutputStream;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.nio.charset.StandardCharsets;

/**
* HandlerInterceptor 拦截器
*/
@Component

//实现HandlerInterceptor接口
public class TokenInterceptor implements HandlerInterceptor {


    @Autowired
    private StringRedisTemplate redisTemplate;

    @Autowired
    private JWTProperties jwtProperties;
    String TOKEN_USER_KEY = "token:user:";


    //拦截方法
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        //从请求头中获取方法token
        String token = request.getHeader(jwtProperties.getHeader());
        if(StrUtil.isEmpty(token)){
            resp(response,"用户未登录");
            return false;
        }

        String userJson = redisTemplate.opsForValue().get(TOKEN_USER_KEY + token);
        if(StrUtil.isEmpty(userJson)){
            resp(response,"用户未登录/登录已过期");
            return false;
        }
        return true;
    }

    private void resp(ServletResponse servletResponse, String msg) throws IOException {
        HttpServletResponse response = (HttpServletResponse) servletResponse;
        ServletOutputStream outputStream = response.getOutputStream();
        response.setStatus(SystemConstant.CODE_401);
        response.setContentType(SystemConstant.JSON);
        R<Object> r = R.fail(msg);
        outputStream.write(JSON.toJSONString(r).getBytes(StandardCharsets.UTF_8));
    }
}

2、编写webMvcConfig
package com.walker.dianping.common.config.mvc;

import com.walker.dianping.common.config.interceptor.TokenInterceptor;
import com.walker.dianping.common.properties.JWTProperties;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class WebMvcConfig implements WebMvcConfigurer {


    //注入token拦截器
    @Autowired
    private TokenInterceptor tokenInterceptor;
    @Autowired
    private JWTProperties jwtProperties;

    /**
    * 重写addInterceptors(InterceptorRegistry registry)方法
    */
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(tokenInterceptor)
                //不需要进行拦截的,也就是白名单
                .excludePathPatterns("/user/login")
                .addPathPatterns("/**");
    }


}

3、测试
现在是将"/user/login"作为白名单,不进行拦截,所以下面做两个测试的方式

  • user/login接口

image.png
返回结果是200,是ok的,因为不需要被拦截

  • test/get接口,不带token时

image.png
可以发现,走了拦截器的方法,因为其未带token,所以返回500和未登录

  • test/get接口,带token时

image.png
可以发现返回200

aop+注解方式

这个是编写一个注解IgnoreToken,然后如果接口中有带该方法的,则认为是白名单,不需要做token的校验

1、导入依赖
     <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>
2、注解类

定义该注解,如果是带有该注解的,则是白名单,不需要进行token的校验

package com.walker.dianping.common.annotation;

import java.lang.annotation.*;

/**
* author:walker
* time: 2023/2/3
* description: 是否需要token
*/

//注解可以存在于方法,以及类中
@Target({ElementType.METHOD,ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface IgnoreToken {


}

3、编写切面类

切面拦截方法大概流程如下:
image.png

package com.walker.dianping.common.aspect;

import cn.hutool.core.util.StrUtil;
import com.alibaba.fastjson.JSON;
import com.walker.dianping.common.annotation.IgnoreToken;
import com.walker.dianping.common.constants.SystemConstant;
import com.walker.dianping.common.properties.JWTProperties;
import com.walker.dianping.entity.R;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.ServletOutputStream;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.lang.reflect.Method;
import java.nio.charset.StandardCharsets;

/**
 * author:walker
 * time: 2023/2/3
 * description:  token切面处理
 */
@Component
//声明切面
@Aspect
public class TokenAspect {

    @Autowired
    private JWTProperties jwtProperties;

    @Autowired
    private StringRedisTemplate redisTemplate;

    String TOKEN_USER_KEY = "token:user:";

    //切点
    //controller.*.*(..)  代表了controller包下的所有类.所有public方法(所有参数)
    //这里需要修改为自己的controller放置的地方
    @Pointcut(value = "execution(public * com.walker.dianping.controller.*.*(..))")
    public void allController() {
        
    }


    /**
    * @Around注解用于修饰Around增强处理,Around增强处理非常强大,表现在:
     * 1. @Around可以自由选择增强动作与目标方法的执行顺序,也就是说可以在增强动作前后,甚至过程中执行目标方法。这个特性的实现在于,调用ProceedingJoinPoint参数的procedd()方法才会执行目标方法。
     * 2. @Around可以改变执行目标方法的参数值,也可以改变执行目标方法之后的返回值。
    */
    @Around("allController()")
    public Object before(ProceedingJoinPoint joinPoint) throws Throwable {
        //获取方法签名和调用的方法
        MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
        Method method = methodSignature.getMethod();
        

        //获取注解
        IgnoreToken annotation = method.getAnnotation(IgnoreToken.class);
        //如果有该注解的,则不需要进行token的校验
        if (annotation != null) {
            return joinPoint.proceed(joinPoint.getArgs());
        }
        //否则需要校验token的情况
        ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();

        //如果不是从前端过来的,没有ServletRequestAttributes,则直接放行?
        //但是这一步需要思考一下,我们的接口一般都提供给前端使用,所以也可以不做该判断处理
        if (requestAttributes == null || requestAttributes.getResponse() == null) {
            return joinPoint.proceed(joinPoint.getArgs());
        }

        HttpServletRequest request = requestAttributes.getRequest();
        HttpServletResponse response = requestAttributes.getResponse();
        //从请求头获取token,如果没有的话,则提示未登录
        String token = request.getHeader(jwtProperties.getHeader());
        if (StrUtil.isEmpty(token)) {
            resp(response,"用户未登录");
            return null;
        }

        //从redis中获取token
        String userJson = redisTemplate.opsForValue().get(TOKEN_USER_KEY + token);
        if(StrUtil.isEmpty(userJson)){
            resp(response,"用户未登录/登录已过期");
            return false;
        }

        return joinPoint.proceed(joinPoint.getArgs());

    }

        private void resp(ServletResponse servletResponse, String msg) throws IOException {
        HttpServletResponse response = (HttpServletResponse) servletResponse;
        ServletOutputStream outputStream = response.getOutputStream();
        response.setStatus(SystemConstant.CODE_401);
        response.setContentType(SystemConstant.JSON);
        R<Object> r = R.fail(msg);
        outputStream.write(JSON.toJSONString(r).getBytes(StandardCharsets.UTF_8));
    }

}

4、编写测试接口

这里只做一个演示,具体的实现逻辑大家自己写,这里就不将代码列出来了

package com.walker.dianping.controller;


import com.walker.dianping.common.annotation.IgnoreToken;
import com.walker.dianping.component.UserComponent;
import com.walker.dianping.entity.R;
import com.walker.dianping.entity.form.UserLoginForm;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

import javax.validation.Valid;

/**
 * <p>
 *  前端控制器
 * </p>
 *
 * @author walker
 * @since 2023-01-30
 */
@RestController
@RequestMapping("/user")
public class TbUserController {

    @Autowired
    private UserComponent userComponent;


    //编写注解,代表不需要进行token的校验
    @IgnoreToken
    @PostMapping("/login")
    public R login(@RequestBody @Valid UserLoginForm form){
        return R.ok(userComponent.login(form));
    }

    @GetMapping("/test")
    public R test(){
        return R.ok("hello");
    }

    @PostMapping("/add")
    public R add(@RequestBody @Valid UserLoginForm form){
        userComponent.add(form);
        return R.ok();
    }


}

5、测试
  • 调用user/test接口,未带token

image.png

  • /user/test 带token

image.png

  • /user/login接口

发现是不需要进行拦截的
image.png

参数解析器

1、编写解析器
  • 先实现HandlerMethodArgumentResolver接口
package com.walker.dianping.common.config.resolver;

import cn.hutool.core.util.StrUtil;
import com.alibaba.fastjson.JSONObject;
import com.walker.dianping.common.annotation.LoginUser;
import com.walker.dianping.common.properties.JWTProperties;
import com.walker.dianping.entity.TbUserEntity;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.MethodParameter;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.support.WebDataBinderFactory;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.method.support.ModelAndViewContainer;

import javax.servlet.http.HttpServletRequest;

/**
 * author:walker
 * time: 2023/2/3
 * description:  参数解析器
 */

@Component
public class LoginUserResolver implements HandlerMethodArgumentResolver {

    @Autowired
    private JWTProperties jwtProperties;

    @Autowired
    private StringRedisTemplate redisTemplate;

    /**
     * 什么是否进行拦截处理,也就是支持的条件,
     * 这里是当有LoginUser注解时
     */
    @Override
    public boolean supportsParameter(MethodParameter parameter) {
        //当有LoginUser的进行进行解析
        return parameter.hasParameterAnnotation(LoginUser.class);
    }

    
//重写解析参数方法
    @Override
    public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {

        //获取请求属性
        ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        if (requestAttributes == null) {
            return null;
        }

        
        HttpServletRequest request = requestAttributes.getRequest();
        //从请求头中获取token
        String token = request.getHeader(jwtProperties.getHeader());
        if (StrUtil.isEmpty(token)) {
            return null;
        }

        //在redis中获取token
        String userJson = redisTemplate.opsForValue().get("token:user:" + token);

        
        if (StrUtil.isEmpty(userJson)) {
            return null;
        }

        //如果token中存储的用户数据存在,则返回
        return JSONObject.parseObject(userJson, TbUserEntity.class);
    }
}

2、webMvcConfig配置
package com.walker.dianping.common.config.mvc;

import com.walker.dianping.common.config.resolver.LoginUserResolver;
import com.walker.dianping.common.properties.JWTProperties;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

import java.util.List;

@Configuration
public class WebMvcConfig implements WebMvcConfigurer {

    /**
     *  参数解析器
     */
    @Autowired
    private LoginUserResolver loginUserResolver;



    /**
     * 参数解析器
     */
    @Override
    public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
        resolvers.add(loginUserResolver);
    }
}

3、测试

  • 测试接口
    @GetMapping("/test")
    //使用@LoginUser注解和TbUserEntity去接收参数
    public R test(@LoginUser TbUserEntity tbUserEntity){
        return R.ok(JSON.toJSONString(tbUserEntity));
    }
  • 使用postman带token发起请求

image.png

发现结果可以获取到解析后的参数

参考文档:
Springboot实现登录拦截的三种方式

我是程序员walker,一个持续学习,分享干货的博主
关注公众号【I am Walker】,一块进步

  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
要通过自定义注解AOP实现Spring Security配置指定接口不需要Token才能访问,可以按照以下步骤进行操作: 1. 创建一个自定义注解,例如`@NoTokenRequired`,用于标识不需要Token的接口。 ```java @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface NoTokenRequired { } ``` 2. 创建一个切面类,用于拦截带有`@NoTokenRequired`注解的方法,并跳过Spring Security的Token验证。 ```java @Aspect @Component public class TokenValidationAspect { @Before("@annotation(com.example.NoTokenRequired)") public void skipTokenValidation(JoinPoint joinPoint) { // 跳过Spring Security的Token验证逻辑 SecurityContextHolder.getContext().setAuthentication(null); } } ``` 3. 配置Spring Security,将AOP切面类添加到Spring Security的配置。 ```java @Configuration @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private TokenValidationAspect tokenValidationAspect; @Override protected void configure(HttpSecurity http) throws Exception { http .authorizeRequests() // 配置需要Token验证的接口 .anyRequest().authenticated() .and() .csrf().disable(); // 将AOP切面类添加到Spring Security的配置 http.addFilterBefore(tokenValidationAspect, UsernamePasswordAuthenticationFilter.class); } } ``` 4. 在需要不需要Token验证的接口上,添加`@NoTokenRequired`注解。 ```java @RestController public class ExampleController { @NoTokenRequired @GetMapping("/example") public String example() { return "This API does not require Token"; } } ``` 这样配置之后,带有`@NoTokenRequired`注解的接口将不会进行Spring Security的Token验证,即可在没有Token的情况下访问该接口。其他接口仍然需要进行Token验证

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

WalkerShen

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值