一、什么是 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("成功封号!");
}
}