欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

Spring Boot+JWT快速实现简单的接口鉴权

程序员文章站 2022-06-28 18:38:52
一般服务的安全包括认证(Authentication)与授权(Authorization)两部分,认证即证明一个用户是合法的用户,比如通过用户名密码的形式,授权则是控制某个用户可以访问哪些资源。比较成熟的框架有Shiro、Spring Security,如果要实现第三方授权模式,则可采用OAuth2。但如果是一些简单的应用,比如一个只需要鉴别用户是否登录的APP,则可以简单地通过注解+拦截器的方式来实现。本文介绍了具体实现过程,虽基于Spring Boot实现,但稍作修改(主要是拦截器配置)就可以引入其它S...

一般服务的安全包括认证(Authentication)与授权(Authorization)两部分,认证即证明一个用户是合法的用户,比如通过用户名密码的形式,授权则是控制某个用户可以访问哪些资源。比较成熟的框架有Shiro、Spring Security,如果要实现第三方授权模式,则可采用OAuth2。但如果是一些简单的应用,比如一个只需要鉴别用户是否登录的APP,则可以简单地通过注解+拦截器的方式来实现。本文介绍了具体实现过程,虽基于Spring Boot实现,但稍作修改(主要是拦截器配置)就可以引入其它Spring MVC的项目。

1. 依赖配置

在pom.xml中添加JWT与redis依赖

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.1.5.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>springboot-auth</artifactId>


    <properties>
        <java.version>1.8</java.version>
        <jwt.version>0.9.1</jwt.version>
        <lombok.version>1.16.18</lombok.version>
        <apache-commons-lang3.version>3.7</apache-commons-lang3.version>
        <commons-collections.version>3.2.2</commons-collections.version>
        <commons-io.version>2.6</commons-io.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
            <version>${jwt.version}</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>${lombok.version}</version>
        </dependency>
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
            <version>${apache-commons-lang3.version}</version>
        </dependency>
        <dependency>
            <groupId>commons-collections</groupId>
            <artifactId>commons-collections</artifactId>
            <version>${commons-collections.version}</version>
        </dependency>
        <dependency>
            <groupId>commons-io</groupId>
            <artifactId>commons-io</artifactId>
            <version>${commons-io.version}</version>
        </dependency>
    </dependencies>

</project>

 

在application.yml配置文件中添加redis相关配置属性

spring:
  redis:
    host: localhost
    port: 6379
    database: 0
    password:
    timeout: 3000
    jedis:
      pool:
        min-idle: 2
        max-idle: 8
        max-active: 8
        max-wait: 1000
jwt:
  secret: mySecret
  expirt: 3600
  authorization: access-token

2. 自定义注解

比如需要登录的接口比较多,就可以定义如 @NoAuth 的注解来标记不需要登录验证,反之则需要鉴权。

package com.kongliand.auth;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/***
 * 不会检查用户是否登录及鉴权
 * @author kevin
 * @Date 2020/7/14 20:40
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface NoAuth {
}

3. 定义token管理器

在登录接口通过时,调用 createToken 创建token,并保存到redis中,设置过期时间,调用 checkToken 来验证,并更新token的过期时间, 退出登录时,删除token。

package com.kongliand.auth;

import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;

import java.util.Date;
import java.util.concurrent.TimeUnit;

/***
 * redis token管理工具
 * @author kevin
 * @Date 2020/7/14 20:40
 */
@Component
public class RedisTokenManager {

    @Autowired
    private StringRedisTemplate redisTemplate;

    @Value("${jwt.secret}")
    private String jwtSecret;

    @Value("${jwt.expirt}")
    private Long expirtTime;

    @Value("${jwt.authorization}")
    private String access_Token;

    /**
     * 生成TOKEN
     */
    public String createToken(String userId) {
        String token = Jwts.builder().setId(userId).setIssuedAt(new Date()).signWith(SignatureAlgorithm.HS256, jwtSecret).compact();
        //存储到redis并设置过期时间
        redisTemplate.boundValueOps(access_Token + ":" + userId).set(token, expirtTime, TimeUnit.SECONDS);
        return token;
    }

    public boolean checkToken(TokenModel model) {
        if (model == null) {
            return false;
        }
        String token = redisTemplate.boundValueOps(access_Token + ":" + model.getUserId()).get();
        if (token == null || !token.equals(model.getToken())) {
            return false;
        }
        //如果验证成功,说明此用户进行了一次有效操作,延长token的过期时间
        redisTemplate.boundValueOps(model.getUserId()).expire(expirtTime,TimeUnit.SECONDS);
        return true;
    }

    public void deleteToken(String userId) {
        redisTemplate.delete(userId);
    }

}

4. 定义拦截器

如果验证通过,则从JWT token中解析出userId,通过AuthUtil工具方法保存到ThreadLocal中,供下游访问。在请求处理结束调用 afterCompletion 方法中,要清除掉ThreadLocal中的值,否则由于线程池的复用,导致被其他用户获取。

package com.kongliand.auth;

import com.kongliand.util.ApiResponse;
import com.kongliand.util.WebUtil;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.lang.reflect.Method;

/***
 *
 * @author kevin
 * @Date 2020/7/14 20:40
 */
@Component
@Slf4j
public class AuthInterceptor extends HandlerInterceptorAdapter {

    @Autowired
    private RedisTokenManager tokenManager;

    @Value("${jwt.secret}")
    private String jwtSecret;

    @Value("${jwt.authorization}")
    private String access_Token;

    public boolean preHandle(HttpServletRequest request,
                             HttpServletResponse response, Object handler) throws Exception {
        String requestPath = request.getRequestURI().substring(request.getContextPath().length());
        // 如果不是映射到方法直接通过
        if (!(handler instanceof HandlerMethod)) {
            return true;
        }

        HandlerMethod handlerMethod = (HandlerMethod) handler;
        Method method = handlerMethod.getMethod();
        // 如果方法注明了 NoAuth,则不需要登录token验证
        if (method.getAnnotation(NoAuth.class) != null) {
            return true;
        }

        // 从header中得到token
        String authorization = request.getHeader(access_Token);
        // 验证token
        if(StringUtils.isBlank(authorization)){
            WebUtil.outputJsonString(ApiResponse.failed("未提供有效Token!"), response);
            return false;
        }
        try {
            Claims claims = Jwts.parser().setSigningKey(jwtSecret).parseClaimsJws(authorization).getBody();
            String userId = claims.getId();
            TokenModel model = new TokenModel(userId, authorization);
            if (tokenManager.checkToken(model)) {
                AuthUtil.setUserId(model.getUserId());
                return true;
            } else {
                WebUtil.outputJsonString(ApiResponse.failed("未提供有效Token!"), response);
                return false;
            }
        } catch (Exception e) {
            WebUtil.outputJsonString(ApiResponse.failed("校验Token发生异常!"), response);
            return false;
        }
    }


    @Override
    public void afterCompletion(
            HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
        //结束后清除,否则由于连接池复用,导致ThreadLocal的值被其他用户获取
        AuthUtil.clear();
    }

}

注册拦截器

package com.kongliand.config;

import com.kongliand.auth.AuthInterceptor;
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;

/***
 *
 * @author kevin
 * @Date 2020/7/14 20:40
 */
@Configuration
public class WebConfiguration implements WebMvcConfigurer {

    private AuthInterceptor authInterceptor;

    @Autowired
    public void setAuthInterceptor(AuthInterceptor authInterceptor){
        this.authInterceptor = authInterceptor;
    }
    /**
    * 注册鉴权拦截器
    * @param
    * @return
    */
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(authInterceptor).addPathPatterns("/**").excludePathPatterns("/error");
    }
}

5. 定义测试controller

package com.kongliand.controller;

import com.kongliand.auth.AuthUtil;
import com.kongliand.auth.NoAuth;
import com.kongliand.auth.RedisTokenManager;

import com.kongliand.util.ApiResponse;
import org.apache.commons.collections.MapUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.Map;

/***
 * 测试controller
 * @author kevin
 * @Date 2020/7/14 20:40
 */

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

    @Autowired
    private RedisTokenManager tokenManager;


    @NoAuth
    @RequestMapping("/login")
    public ApiResponse login(@RequestBody Map<String, Object> params) {
        String username = MapUtils.getString(params, "username");
        String password = MapUtils.getString(params, "password");
        if ("kevin".equals(username) && "123456".equals(password)) {
            return ApiResponse.success(tokenManager.createToken(username));
        } else {
            return ApiResponse.failed("用户名或密码错误");
        }
    }

    @NoAuth
    @RequestMapping("/no-auth")
    public ApiResponse skipAuth() {
        return ApiResponse.success("不需要认证的接口调用");
    }

    @RequestMapping("/auth")
    public ApiResponse needAuth() {
        return ApiResponse.success("username: " + AuthUtil.getUserId());
    }
}

6. 验证

登录接口

Spring Boot+JWT快速实现简单的接口鉴权 
测试接口

Spring Boot+JWT快速实现简单的接口鉴权

本文地址:https://blog.csdn.net/kongliand/article/details/107348987