Jwt 如何在 springboot 项目中进行接口访问鉴权

结合以下文章:
jwt.io 官网详细介绍

SpringBoot集成JWT实现Token登录验证

SpringBoot项目使用JWT+拦截器实现token验证

spring-boot + JWT实现TOKEN登录接口验证

SpringBoot集成JWT实现token验证

SpringBoot 开发 – JWT 认证教程

1 springboot 框架负责接口的拦截和放行

1.1 原理

使用 HandlerInterceptor (对于 springboot 框架不推荐使用 doFilter)

1.2 思路

白名单思路, 拦截所有接口目录/**, 放行需要的接口

@Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(authenticationInterceptor())
                .addPathPatterns("/**");
}

1.3 坑: Springboot 访问了错误处理路径 /error

接口程序中有未处理的异常, 报了 Null Pointer Exception, Springboot 调用默认的错误处理接口 /error 企图调用错误处理程序, 第二次触发了 HandlerInterceptor, 由于/error不带 token, 所以被拒绝,最终报 token 校验不通过的错误信息.

这里的解决方法:

1 处理程序中所有异常, 在最外层捕捉不可预见的异常, 返回统一错误信息,服务内部错误
2 自定义 springboot 的 error path 为符合自己程序的路径, 并用 controller 定义处理程序. 当springbot框架本身或者依赖包出现不可预知的错误时,转到这里, 可以返回统一错误信息

其它方法也可以尝试使用 @ControllerAdvice 自定义异常处理类, 处理程序自身不可预知的错误

2 jwt token 负责携带数据和签名的生成及校验

官方库

<dependency>
    <groupId>com.auth0</groupId>
    <artifactId>java-jwt</artifactId>
    <version>3.19.4</version>
</dependency>

2.1 初始化

JWTCreator.Builder builder = JWT.create();

2.2 设置 Header

Map<String,Object> headerMap = new HashMap<>();
builder.withHeader(headerMap);

2.3 携带数据 payload

自定义数据

for (Map.Entry<String,String> entry:data.entrySet()) {
    builder.withClaim(entry.getKey(), entry.getValue());
}

设置过期时间

builder.withExpiresAt(expireDate);

Token 放在请求Header的Authorization字段里。Token 携带数据userId

Token 的格式:

header

{
  "kid": "XXXXXXXXXXXXXXXXXX0MDVmLWIyMjEtMjQ1MWU3NWYxXXXXX5",
  "typ": "JWT",
  "alg": "RS256"
}

payload

{
  "exp": 1684829637,
  "userId": "xxxxxxxxxxxx==",
  "iat": 1684829607
}

2.4 签名 sign 后, 生成 token

token = builder.sign(Algorithm.HMAC256(secretKey))

如果使用RSA非对称算法,可以从jwt库的源码看出使用私钥签名

token = builder.sign(Algorithm.RSA256(rsaPrivateKey))

java-jwt-4.0.0-sources.jar!/com/auth0/jwt/algorithm/RSAAlgorithm.java

    @Override
    public byte[] sign(byte[] headerBytes, byte[] payloadBytes) throws SignatureGenerationException {
        try {
            RSAPrivateKey privateKey = keyProvider.getPrivateKey();
            if (privateKey == null) {
                throw new IllegalStateException("The given Private Key is null.");
            }
            return crypto.createSignatureFor(getDescription(), privateKey, headerBytes, payloadBytes);
        } catch (NoSuchAlgorithmException | SignatureException | InvalidKeyException | IllegalStateException e) {
            throw new SignatureGenerationException(this, e);
        }
    }

2.5 校验

配置算法

JWTVerifier verifier = JWT.require(Algorithm.HMAC256(secretKey)).build();

如果使用RSA非对称算法,可以从jwt库的源码看出使用公钥校验

JWTVerifier verifier = JWT.require(Algorithm.RSA256(rsaPublicKey)).build();

java-jwt-4.0.0-sources.jar!/com/auth0/jwt/algorithm/RSAAlgorithm.java

    @Override
    public void verify(DecodedJWT jwt) throws SignatureVerificationException {
        try {
            byte[] signatureBytes = Base64.getUrlDecoder().decode(jwt.getSignature());
            RSAPublicKey publicKey = keyProvider.getPublicKeyById(jwt.getKeyId());
            if (publicKey == null) {
                throw new IllegalStateException("The given Public Key is null.");
            }
            boolean valid = crypto.verifySignatureFor(
                    getDescription(), publicKey, jwt.getHeader(), jwt.getPayload(), signatureBytes);
            if (!valid) {
                throw new SignatureVerificationException(this);
            }
        } catch (NoSuchAlgorithmException | SignatureException | InvalidKeyException
                | IllegalArgumentException | IllegalStateException e) {
            throw new SignatureVerificationException(this, e);
        }
    }

校验

DecodedJWT decodedJWT  = verifier.verify(token);

校验的方法是再生成一遍进行比较

2.6 获取信息

两种方法

  • 第一种方法: 在 HandlerInterceptor 里的 PreHandle 校验通过后, 立即解析 token, 拿到数据. 把解析结果放入 threadlocal 变量, 这样在整个请求的主线程里, 可以使用该变量, 并且该变量对其它线程不可见, 在请求结束的 afterCompletion() 方法里把 threadlocal 变量注销释放.
  • 第二种方法: 在需要获取信息的时候, 先获取该severlet请求的上下文 RequestContextHolder, 进而拿到请求Request中的 header, 进而拿到 token, 重新解析 token, 获取数据. 由于接口进来时, 已经通过校验, 可以不通过校验的方式获取解析后的token, 直接调用解析方法进行解析即可.

2.7 字段说明

nbf 可用于多机部署时, 服务器之间时间的微小差异

3 拦截器代码

Springboot 建议使用HandlerInterceptor进行拦截

定义 annotation, 对这个annotation 修饰的接口进行拦截


@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface AccessWithoutToken {
    boolean required() default true;
}

@Slf4j
public class AuthenticationInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
        log.info("Request from {} to URI: {}, URL: {}", HttpClientUtil.getRemoteIp(request), request.getRequestURI(), request.getRequestURL().toString());
        // 如果不是映射到方法直接通过
        if (!(handler instanceof HandlerMethod)) {
            return true;
        }

        HandlerMethod handlerMethod = (HandlerMethod) handler;
        Method method = handlerMethod.getMethod();
        log.info("Method {}, {}", method.getName(), method.getDeclaredAnnotations());
        if (method.isAnnotationPresent(AccessWithoutToken.class)) {
            AccessWithoutToken accessWithoutToken = method.getAnnotation(AccessWithoutToken.class);
            if (accessWithoutToken.required()) {
                return true;
            }
        }

        // Authorization: Bearer <token>
        String authorization = request.getHeader("Authorization");

        // TODO check token
        response.setCharacterEncoding("UTF-8");
        response.setContentType("application/json; charset=utf-8");
        if (!org.apache.commons.lang3.StringUtils.isBlank(authorization)) {

            String[] authorizationStr= StringUtils.split(authorization, SPACE);
            if (2 == authorizationStr.length) {
                String authType = authorizationStr[0];
                String token = authorizationStr[1];

                if (authType.equals("Bearer") && !org.apache.commons.lang3.StringUtils.isBlank(token)) {

                    DecodedJWT decodedJWT = JwtUtil.verifyToken(token);
                    if (null != decodedJWT) {
                        // TODO 校验通过获取信息
                        log.info("token: {}......, 校验通过, 签发时间{}, userId{}", token.substring(0, 32), decodedJWT.getIssuedAt().getTime(), decodedJWT.getClaim("userId"));
                        return true;
                    }
                } else {
                    log.error("Token 校验失败, auth prefix={}, token={}", authType, token);
                }
            } else {
                log.error("Token 校验失败, http header 中解析 Authorization 字段错误, authorization={}", authorization);
            }

        } else {
            log.error("Token 校验失败, http header 中没有 Authorization 字段, authorization={}", authorization);
        }
        try (PrintWriter writer = response.getWriter()) {
            writer.print(RestResponse.fail(RestCode.USER_VALIDATE_FAIL_JWT_TOKEN));
        } catch (Exception e) {
            log.error("登录 JWT Token 校验失败 未知错误 error=", e);
        }

        return false;
    }
}

扩展阅读

OWASP Top Ten 2021 : Related Cheat Sheets

okta What-is-the-lifetime-of-the-JWT-tokens

其它

关于springboot 默认 error path

customize springboot default error path
ErrorMvcAutoConfiguration.java
get-started-with-custom-error-handling-in-spring-boot-java/
spring-boot-custom-error-page
how-to-fix-spring-boot-customize-http-error-response-in-java
howto.actuator.customize-whitelabel-error-page
boot-features-error-handling

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
为了实现SpringBoot+Vue前后端分离的登录界面使用JWT鉴权,需要进行以下步骤: 1.在SpringBoot配置SpringSecurity,包括用户认证和授权,可以参考引用的实现方式。 2.在SpringBoot配置JWT,包括生成Token和解析Token,可以使用jjwt库实现,可以参考引用的maven依赖配置。 3.在Vue实现登录界面,包括输入用户名和密码,发送POST请求到后端验证用户信息,并获取Token。 4.在Vue使用获取到的Token进行后续请求的鉴权,可以在请求头添加Authorization字段,值为Bearer加上Token。 下面是一个简单的示例代码,仅供参考: 1. SpringBoot的配置 ```java @Configuration @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private UserDetailsService userDetailsService; @Autowired private JwtAuthenticationEntryPoint unauthorizedHandler; @Bean public JwtAuthenticationFilter jwtAuthenticationFilter() { return new JwtAuthenticationFilter(); } @Override public void configure(AuthenticationManagerBuilder authenticationManagerBuilder) throws Exception { authenticationManagerBuilder .userDetailsService(userDetailsService) .passwordEncoder(passwordEncoder()); } @Bean(BeanIds.AUTHENTICATION_MANAGER) @Override public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); } @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } @Override protected void configure(HttpSecurity http) throws Exception { http .cors() .and() .csrf() .disable() .exceptionHandling() .authenticationEntryPoint(unauthorizedHandler) .and() .sessionManagement() .sessionCreationPolicy(SessionCreationPolicy.STATELESS) .and() .authorizeRequests() .antMatchers("/api/auth/**") .permitAll() .anyRequest() .authenticated(); // Add our custom JWT security filter http.addFilterBefore(jwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class); } } ``` 2. Vue的登录界面 ```html <template> <div> <h2>Login</h2> <form @submit.prevent="login"> <div> <label>Username:</label> <input type="text" v-model="username" required> </div> <div> <label>Password:</label> <input type="password" v-model="password" required> </div> <button type="submit">Login</button> </form> </div> </template> <script> import axios from 'axios'; export default { data() { return { username: '', password: '' }; }, methods: { async login() { try { const response = await axios.post('/api/auth/login', { username: this.username, password: this.password }); const token = response.data.token; localStorage.setItem('token', token); axios.defaults.headers.common['Authorization'] = `Bearer ${token}`; this.$router.push('/'); } catch (error) { console.error(error); } } } }; </script> ``` 3. Vue的请求鉴权 ```javascript import axios from 'axios'; const token = localStorage.getItem('token'); if (token) { axios.defaults.headers.common['Authorization'] = `Bearer ${token}`; } axios.get('/api/protected/resource') .then(response => { console.log(response.data); }) .catch(error => { console.error(error); }); ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值