JWT 在 Java-web 项目中的简单实际应用(改进版)

一、什么是 JWT

双方之间传递安全信息的简洁的、URL安全的表述性声明规范。JWT 作为一个开放的标准(RFC 7519),定义了一种简洁的,自包含的方法用于通信双方之间以 Json 对象的形式安全的传递信息。简洁(Compact): 可以通过 URL,POST 参数或者在 HTTP header 发送,因为数据量小,传输速度也很快 自包含(Self-contained):负载中包含了所有用户所需要的信息,避免了多次查询数据库。

二、JWT 在 java-web 项目中的简单使用

第一步:引入maven依赖

;

    io.jsonwebtoken
    jjwt
    0.9.1


    com.auth0
    java-jwt
    3.4.0

第二步:借鉴 Shiro 的源码,创建几个注解(@RequiresUser 没用上),拦截器通过这些注释区分是否进行权限拦截,并决定进行何种程度的校验。

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

/**
 * 游客身份即可进行的操作
 */
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface RequiresGuest {
}
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * 需要登录校验后,才有权进行的操作
 */
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface RequiresAuthentication {
}
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * 需要某个或某些身份,才有权进行的操作。身份与身份之间可以有 AND 和 OR 的关系
 * For example,
 *
 * RequiresRoles("aRoleName")
 * void someMethod() {
 *     ...
 * }
 *
 */
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface RequiresRoles {

    String[] value();

    Logical logical() default Logical.AND;
}
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * 类似上一个。需要某个或某些权限,才能进行的操作。
 */
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface RequiresPermissions {

    String[] value();

    Logical logical() default Logical.AND;
}
/**
 * 表示身份与身份、权限与权限之间的 AND 和 OR 的关系。
 */
public enum Logical {
    AND, OR
}

第三步:编写 JWTUtil 工具类(生成 token、解析 Token 从中获取 Claim、 校验 Token)

@Slf4j
public class JWTUtil {

    /*
     * 生成签名的时候使用的秘钥 secret,这个方法本地封装了的,一般可以从本地配置文件中读取,切记这个秘钥不能外露哦。
     * 它就是你服务端的私钥,在任何场景都不应该流露出去。一旦客户端得知这个secret, 那就意味着客户端是可以自我签发 jwt 了。
     * 该值根据具体情况可改,此处写死只是临时举例用。
     */
    private static final String SECRET_KEY = "123456";

    /*
     * 默认过期时间: 24 小时
     */
    private static final long EXPIRE_TIME = 24 * 60 * 60 * 1000;

    /**
     * 用户登录成功后使用 HS256 算法生成 token,ttlMillis 秒后
     * 在 token 中存入用户登录的登录名 LoginUserName
     */
    public static String createToken(Long ttlMillis, String username) {

        ttlMillis = MoreObjects.firstNonNull(ttlMillis, EXPIRE_TIME);

        Date date = new Date(System.currentTimeMillis() + ttlMillis);
        Algorithm algorithm = Algorithm.HMAC256(SECRET_KEY);

        return JWT.create()
                .withClaim("username", username) // 附带 username 信息
                .withExpiresAt(date)                   // 到期时间
                .sign(algorithm);                      // 创建一个新的 JWT,并使用给定的算法进行标记
    }

    /**
     * 校验 token 是否正确
     *
     */
    public static boolean verify(String token, String username) {
        try {
            Algorithm algorithm = Algorithm.HMAC256(SECRET_KEY);
            //在token中附带了username信息
            JWTVerifier verifier = JWT.require(algorithm)
                    .withClaim("username", username)
                    .build();
            // 验证 token
            verifier.verify(token);
            return true;
        } catch (Exception exception) {
            return false;
        }
    }

    public static boolean verify(String token, Long userID) {
        try {
            Algorithm algorithm = Algorithm.HMAC256(SECRET_KEY);
            //在token中附带了username信息
            JWTVerifier verifier = JWT.require(algorithm)
                    .withClaim("user-id", userID)
                    .build();
            // 验证 token
            verifier.verify(token);
            return true;
        } catch (Exception exception) {
            return false;
        }

    }


    /**
     * 解析 token,从中获得 username,无需 SECRET_KEY 解密也能获得
     */
    public static String getUsername(String token) {
        try {
            DecodedJWT jwt = JWT.decode(token);
            return jwt.getClaim("username").asString();
        } catch (JWTDecodeException e) {
            return null;
        }
    }


    public static Long getUserID(String token) {
        try {
            DecodedJWT jwt = JWT.decode(token);
            return jwt.getClaim("user-id").asLong();
        } catch (JWTDecodeException e) {
            return null;
        }
    }
}

第四步:编写拦截器拦截请求进行权限验证

拦截校验逻辑:

  • 方式使用了 @RequiresGuest 注解的,不强求请求中必须附带 Token 。
  • 方式使用了 @RequiresAuthentication 注解的,请求中必须附带 Token,且 Token 必须合法。
  • 方式使用了 @RequiresRoles 注解的,在 @RequiresAuthentication 基础上要求当前用户必须具有指定身份。
    方式使用了 @RequiresPermissions 注解的,在 @RequiresAuthentication 基础上要求当前用户必须具有指定权限。

以下代码可再进一步优化逻辑。

@Slf4j
public class AuthenticationInterceptor implements HandlerInterceptor {

    @Autowired
    private UserRepository userRepository;

    @Override
    public boolean preHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object object) throws Exception {
        // 如果拦截的不是方法则直接通过
        if (!(object instanceof HandlerMethod)) {
            return true;
        }

        // 从 http 请求头中取出 token
        String token = httpServletRequest.getHeader("token");
        HandlerMethod handlerMethod = (HandlerMethod) object;
        Method method = handlerMethod.getMethod();

        // 代表,需要登录认证后才能进行的操作。即,游客无法进行该操作。
        if (method.isAnnotationPresent(RequiresAuthentication.class)) {
            checkAuthentication(token);
        }

        // 代表,需要某些身份/角色才能进行的操作。不是该角色的用户无法操作。
        else if (method.isAnnotationPresent(RequiresRoles.class)) {
            RequiresRoles requiresRoles = method.getAnnotation(RequiresRoles.class);
            String[] requiresRoleNames = requiresRoles.value();
            Logical logical = requiresRoles.logical();

            if (!hasRoles(token, Sets.newHashSet(requiresRoleNames), logical)) {
                log.info("权限验证失败:不具备指定身份 {}", Arrays.toString(requiresRoleNames));
                throw new AuthorizationException();
            }

        }
        // 代表,需要某种权限才能进行的操作。没有拥有该角色的用户无法操作。
        else if (method.isAnnotationPresent(RequiresPermissions.class)) {
            RequiresPermissions requiresPermissions = method.getAnnotation(RequiresPermissions.class);
            String[] requiresPermissionNames = requiresPermissions.value();
            Logical logical = requiresPermissions.logical();

            if(!hasPermissions(token, Sets.newHashSet(requiresPermissionNames), logical)) {
                log.info("权限验证失败:不具备指定权限 {}", Arrays.toString(requiresPermissionNames));
                throw new AuthorizationException();
            }
        }
        // 游客/匿名用户 可以进行的操作
        else {
            return true;
        }

        return false;
    }

    @Override
    public void postHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, ModelAndView modelAndView) throws Exception {

    }

    @Override
    public void afterCompletion(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, Exception e) throws Exception {

    }

    /**
     * 有 token,且 token 合法,即代表曾经登录过。
     */
    private User checkAuthentication(String token) {
        if (Strings.isNullOrEmpty(token)) {
            throw new AuthenticationException("无 token,请重新登录");
        }

        // 获取 token 中的 user id
        Long userId = JWTUtil.getUserID(token);
        if (userId == null) {
            throw new AuthenticationException("未登录");
        }

        User user =  userRepository.selectByPrimaryKey(userId);
        if (user == null) {
            throw new AuthenticationException("用户不存在,请重新登录");
        }

        if (!JWTUtil.verify(token, user.getId())) {
            throw new AuthenticationException("非法访问(token 非法)!");
        }

        return user;
    }

    private boolean hasRoles(String token, Set requiredRoleNames, Logical logical) {
        User user = checkAuthentication(token);
        if (user == null)
            return false;

        Set roleNames = user.getRoleNames();

        if (logical == Logical.AND)
            return roleNames.containsAll(requiredRoleNames);
        else
            return !Sets.intersection(roleNames, requiredRoleNames).isEmpty();
    }

    private boolean hasPermissions(String token, Set requiredPermissionNames, Logical logical) {
        User user = checkAuthentication(token);
        if (user == null)
            return false;

        Set permissionNames = user.getPermissionNames();
        if (logical == Logical.AND)
            return permissionNames.containsAll(requiredPermissionNames);
        else
            return !Sets.intersection(permissionNames, requiredPermissionNames).isEmpty();
    }
}

配置拦截器(ps:我使用的是 springboot,大家使用 ssm 配置拦截器的方式不一样)

package com.pjb.springbootjjwt.interceptorconfig;

import com.pjb.springbootjjwt.interceptor.AuthenticationInterceptor;
import org.springframework.context.annotation.Bean;
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 InterceptorConfig implements WebMvcConfigurer {

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(authenticationInterceptor())
                .addPathPatterns("/**");    // 拦截所有请求,通过判断是否有 @RequiresAuthentication 等注解,决定是否需要登录。
    }

    @Bean
    public AuthenticationInterceptor authenticationInterceptor() {
        return new AuthenticationInterceptor();
    }
}

第五步:在示例Controller中的实际应用

@Slf4j
@RestController
@RequestMapping("/admin")
public class AdminController {

    @Autowired
    private ShiroUserRepository userRepository;

    @RequestMapping("/getUser")
    @RequiresRoles("admin")
    public ResultMap getUser() {
        List list = userRepository.selectByExample(null);
        return new ResultMap().success(null).status(200).message(list);
    }

    /**
     * 封号操作
     */
    @RequestMapping("/banUser")
    @RequiresRoles("admin")
    public ResultMap updatePassword(String username) {
        log.info("[管理员] 执行 [封号] 操作");

        return new ResultMap().success(null).status(200).message("成功封号!");
    }
}

你可能感兴趣的:(JWT 在 Java-web 项目中的简单实际应用(改进版))