一言以蔽之,JWT 可以携带非敏感信息,并具有不可篡改性。可以通过验证是否被篡改,以及读取信息内容,完成网络认证的三个问题:“你是谁”、“你有哪些权限”、“是不是冒充的”。
为了安全,使用它需要采用 Https 协议,并且一定要小心防止用于加密的密钥泄露。
采用 JWT 的认证方式下,服务端并不存储用户状态信息,有效期内无法废弃,有效期到期后,需要重新创建一个新的来替换。
所以它并不适合做长期状态保持,不适合需要用户踢下线的场景,不适合需要频繁修改用户信息的场景。因为要解决这些问题,总是需要额外查询数据库或者缓存,或者反复加密解密,强扭的瓜不甜,不如直接使用 Session。不过作为服务间的短时效切换,还是非常合适的,就比如 OAuth 之类的。
通过填写用户名和密码登录。

针对这个登录验证的实现,需要引入 Spring Security、jackson、java-jwt 三个包。
org.springframework.boot spring-boot-starter-security com.fasterxml.jackson.core jackson-core 2.12.1 com.auth0 java-jwt 3.12.1
要验证用户前,自然是先要创建用户实体对象,以及获取用户的服务类。不同的是,这两个类需要实现 Spring Security 的接口,以便将它们集成到验证框架中。
用户实体类需要实现 ”UserDetails“ 接口,这个接口要求实现 getUsername、getPassword、getAuthorities 三个方法,用以获取用户名、密码和权限。以及 isAccountNonExpired`isAccountNonLocked、isCredentialsNonExpired、isEnabled 这四个判断是否是有效用户的方法,因为和验证无关,所以先都返回 true。这里图方便,用了 lombok。
@Data
public class User implements UserDetails {
private static final long serialVersionUID = 1L;
private String username;
private String password;
private Collection extends GrantedAuthority> authorities;
...
}
用户服务类需要实现 “UserDetailsService” 接口,这个接口非常简单,只需要实现 loadUserByUsername(String username) 这么一个方法。这里使用了 MyBatis 来连接数据库获取用户信息。
@Service
public class UserService implements UserDetailsService {
@Autowired
UserMapper userMapper;
@Override
@Transactional
public User loadUserByUsername(String username) {
return userMapper.getByUsername(username);
}
...
}
这个工具类主要负责 token 的生成,验证,从中取值。
@Component
public class JwtTokenProvider {
private static final long JWT_EXPIRATION = 5 * 60 * 1000L; // 五分钟过期
public static final String TOKEN_PREFIX = "Bearer "; // token 的开头字符串
private String jwtSecret = "XXX 密钥,打死也不能告诉别人";
...
}
生成 JWT:从以通过验证的认证对象中,获取用户信息,然后用指定加密方式,以及过期时间生成 token。这里简单的只加了用户名这一个信息到 token 中:
public String generateToken(Authentication authentication) {
User userPrincipal = (User) authentication.getPrincipal(); // 获取用户对象
Date expireDate = new Date(System.currentTimeMillis() + JWT_EXPIRATION); // 设置过期时间
try {
Algorithm algorithm = Algorithm.HMAC256(jwtSecret); // 指定加密方式
return JWT.create().withExpiresAt(expireDate).withClaim("username", userPrincipal.getUsername())
.sign(algorithm); // 签发 JWT
} catch (JWTCreationException jwtCreationException) {
return null;
}
}
验证 JWT:指定和签发相同的加密方式,验证这个 token 是否是本服务器签发,是否篡改,或者已过期。
public boolean validateToken(String authToken) {
try {
Algorithm algorithm = Algorithm.HMAC256(jwtSecret); // 和签发保持一致
JWTVerifier verifier = JWT.require(algorithm).build();
verifier.verify(authToken);
return true;
} catch (JWTVerificationException jwtVerificationException) {
return false;
}
}
获取荷载信息:从 token 的荷载部分里解析用户名信息,这部分是 md5 编码的,属于公开信息。
public String getUsernameFromJWT(String authToken) {
try {
DecodedJWT jwt = JWT.decode(authToken);
return jwt.getClaim("username").asString();
} catch (JWTDecodeException jwtDecodeException) {
return null;
}
}
登录部分需要创建三个文件:负责登录接口处理的拦截器,登陆成功或者失败的处理类。
Spring Security 默认自带表单登录,负责处理这个登录验证过程的过滤器叫“UsernamePasswordAuthenticationFilter”,不过它只支持表单传值,这里用自定义的类继承它,使其能够支持 JSON 传值,负责登录验证接口。
这个拦截器只需要负责从请求中取值即可,验证工作 Spring Security 会帮我们处理好。