一般服务的安全包括认证(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 认证授权 spring boot 接口权限认证_spring

 

测试接口

spring boot 认证授权 spring boot 接口权限认证_spring_02