SpringBoot 整合 JWT
1 什么是 JWT
JSON Web Token(JWT)是一个非常轻巧的规范,这个规范允许我们使用 JWT 在用户和服务器之间传递安全可靠的信息,在 Java 世界中通过 JJWT 实现 JWT 创建和验证。
2 快速上手
2.1 pom.xml
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1<version>
</dependency>
2.2 测试类
import io.jsonwebtoken.*;
import java.util.Date;
/**
* @author PkyShare
* @date 2020/1/3 0003 16:06
*/
public class JwtTest {
private final static String key = "share"; // 秘钥
public static void main(String[] args) {
String token = created();
parse(token);
}
/**
* 创建token
*/
public static String created() {
JwtBuilder jwtBuilder = Jwts.builder().setId("123456") // 设置ID
.setSubject("PkyShare") // 存放的内容
.setIssuedAt(new Date()) // 签名签发时间
.signWith(SignatureAlgorithm.HS256, key) // 加密算法以及秘钥
.claim("school", "gcd"); // 自定义内容,key-value 形式
String token = jwtBuilder.compact();
System.out.println(token); // 创建 JwtBuilder 对象并打印
return token;
}
/**
* 解析 token
* @param token
*/
public static void parse(String token) {
Claims claims = Jwts.parser().setSigningKey(key).parseClaimsJws(token).getBody();
System.out.println(claims.getId());
System.out.println(claims.getSubject());
System.out.println(claims.getIssuedAt());
String school = (String) claims.get("school");
System.out.println("school----" + school);
}
}
2.3 测试结果
3 SpringBoot 整合 JWT
以上简单的使用我们基本了解了 token 的生成,接下来完成一个简单的登录逻辑。
3.1 pom.xml
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.2.0</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
3.2 JwtUtil 工具类
import com.huanda.chetaijitoc.commons.domain.userdb.UserInfo;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtBuilder;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
/**
* JWT 工具类
* @author PkyShare
* @date 2020/1/3 0003 16:39
*/
public class JwtUtil {
//token 密钥
private static final String TOKEN_SECRET = "dfsjeo329safei22kdfeiajdeie1";
//15分钟超时时间
private static final long OUT_TIME = 150 * 60 * 1000;
/**
* 用户登录成功后生成Jwt
* 使用Hs256算法 私匙使用用户密码
* @param user 登录成功的user对象
* @return
*/
public static String createJWT(UserInfo user) {
//指定签名的时候使用的签名算法,也就是header那部分,jjwt已经将这部分内容封装好了。
SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
//生成JWT的时间
long nowMillis = System.currentTimeMillis();
Date now = new Date(nowMillis);
//创建payload的私有声明(根据特定的业务需要添加,如果要拿这个做验证,一般是需要和jwt的接收方提前沟通好验证方式的)
Map<String, Object> claims = new HashMap<>();
claims.put("id", user.getId());
claims.put("username", user.getUsername());
claims.put("password", user.getPassword());
//生成签名的时候使用的秘钥secret,这个方法本地封装了的,一般可以从本地配置文件中读取,切记这个秘钥不能外露哦。它就是你服务端的私钥,在任何场景都不应该流露出去。一旦客户端得知这个secret, 那就意味着客户端是可以自我签发jwt了。
// String key = user.getPassword();
//生成签发人
String subject = user.getUsername();
//下面就是在为payload添加各种标准声明和私有声明了
//这里其实就是new一个JwtBuilder,设置jwt的body
JwtBuilder builder = Jwts.builder()
//如果有私有声明,一定要先设置这个自己创建的私有的声明,这个是给builder的claim赋值,一旦写在标准的声明赋值之后,就是覆盖了那些标准的声明的
.setClaims(claims)
//设置jti(JWT ID):是JWT的唯一标识,根据业务需要,这个可以设置为一个不重复的值,主要用来作为一次性token,从而回避重放攻击。
.setId(UUID.randomUUID().toString())
//iat: jwt的签发时间
.setIssuedAt(now)
//代表这个JWT的主体,即它的所有人,这个是一个json格式的字符串,可以存放什么userid,roldid之类的,作为什么用户的唯一标志。
.setSubject(subject)
//设置签名使用的签名算法和签名使用的秘钥
.signWith(signatureAlgorithm, TOKEN_SECRET);
long expMillis = nowMillis + OUT_TIME;
Date exp = new Date(expMillis);
//设置过期时间
builder.setExpiration(exp);
return builder.compact();
}
/**
* Token的解密
* @param token 加密后的token
* @return
*/
public static Claims parseJWT(String token) {
//签名秘钥,和生成的签名的秘钥一模一样
//得到DefaultJwtParser
Claims claims = Jwts.parser()
//设置签名的秘钥
.setSigningKey(TOKEN_SECRET)
//设置需要解析的jwt
.parseClaimsJws(token).getBody();
return claims;
}
/**
* 校验token
* 在这里可以使用官方的校验,我这里校验的是token中携带的密码于数据库一致的话就校验通过
* @param claims Payload(载荷)
* @param user
* @return
*/
public static Boolean isVerify(Claims claims, UserInfo user) {
if (claims.get("password").equals(user.getPassword())) {
return true;
}
return false;
}
}
3.3 拦截器
除了登录、注册请求外,其他请求都需要携带 token,因此需要设置一个拦截器进行拦截。
3.3.1 @LoginToken 登录注解
方法上用到该注解的则跳过 token 验证。
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* @author PkyShare
* @date 2020/1/3 0003 17:25
*/
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface LoginToken {
boolean required() default true;
}
3.3.2 @CheckToken 校验注解
方法上用到该注解的则校验 token。
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* @author PkyShare
* @date 2020/1/3 0003 17:24
*/
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface CheckToken {
boolean required() default true;
}
3.3.3 拦截器配置
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;
/**
* @author PkyShare
* @date 2020/1/3 0003 17:49
*/
@Configuration
public class InterceptorConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(authenticationInterceptor())
.addPathPatterns("/**"); // 拦截所有请求,通过判断是否有 @LoginRequired 注解 决定是否需要登录
}
@Bean
public AuthenticationInterceptor authenticationInterceptor() {
return new AuthenticationInterceptor();
}
}
3.3.4 Token 拦截器
import com.alibaba.fastjson.JSONObject;
import com.huanda.chetaijitoc.admin.annotation.CheckToken;
import com.huanda.chetaijitoc.admin.annotation.LoginToken;
import com.huanda.chetaijitoc.admin.annotation.QueryToken;
import com.huanda.chetaijitoc.commons.constant.HttpStatus;
import com.huanda.chetaijitoc.commons.domain.userdb.UserInfo;
import com.huanda.chetaijitoc.commons.service.userdb.UserInfoService;
import com.huanda.chetaijitoc.commons.service.utils.JwtUtil;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.SignatureException;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.PrintWriter;
import java.lang.reflect.Method;
/**
* token 拦截器
* @author PkyShare
* @date 2020/1/3 0003 17:28
*/
public class AuthenticationInterceptor implements HandlerInterceptor{
@Autowired
UserInfoService userService;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object object) throws Exception {
// 从 http 请求头中取出 token
String token = request.getHeader("token");
// 如果不是映射到方法直接通过
if (!(object instanceof HandlerMethod)) {
return true;
}
HandlerMethod handlerMethod = (HandlerMethod) object;
Method method = handlerMethod.getMethod();
//检查是否有LoginToken注释,有则跳过认证
if (method.isAnnotationPresent(LoginToken.class)) {
LoginToken loginToken = method.getAnnotation(LoginToken.class);
if (loginToken.required()) {
return true;
}
}
//检查是否有QueryToken注释,有则判断token
if(method.isAnnotationPresent(QueryToken.class)) {
QueryToken queryToken = method.getAnnotation(QueryToken.class);
if(queryToken.required()) {
return setRequest(request, response, token);
}
}
//检查有没有需要用户权限的注解
if (method.isAnnotationPresent(CheckToken.class)) {
CheckToken checkToken = method.getAnnotation(CheckToken.class);
if (checkToken.required()) {
return checkToken(request, response, token);
}
}
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object o, ModelAndView modelAndView) throws Exception {
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object o, Exception e) throws Exception {
}
/**
* 违章查询时设置request
* @param request
* @param response
* @param token
* @return
* @throws Exception
*/
private boolean setRequest(HttpServletRequest request, HttpServletResponse response, String token) throws Exception {
if(StringUtils.isBlank(token)) {
return true;
}
return checkToken(request, response, token);
}
/**
* 校验 token
* @param request
* @param response
* @param token
* @return
*/
private boolean checkToken(HttpServletRequest request, HttpServletResponse response, String token) throws Exception {
// 执行认证
if (StringUtils.isBlank(token)) {
setResultJson(response, HttpStatus.MISSING_PARAMS.getCode(), "Token 不可为空");
return false;
}
// 获取 token 中的 user id
Long userId;
Claims claims;
try {
claims = JwtUtil.parseJWT(token);
userId = (Long) claims.get("id");
}
catch (ExpiredJwtException e) { // 签名过期
setResultJson(response, HttpStatus.TOKEN_EXPIRED.getCode(), HttpStatus.TOKEN_EXPIRED.getTitle());
return false;
}
catch (SignatureException e) { // 签名错误
setResultJson(response, HttpStatus.TOKEN_ERROR.getCode(), HttpStatus.TOKEN_ERROR.getTitle());
return false;
}
catch (Exception e) { // 其他未知异常
setResultJson(response, HttpStatus.ABNORMAL_ACCESS.getCode(), HttpStatus.ABNORMAL_ACCESS.getTitle());
return false;
}
UserInfo user = userService.getById(userId);
if (user == null) {
setResultJson(response, HttpStatus.USER_NOT_EXIT.getCode(), HttpStatus.USER_NOT_EXIT.getTitle());
return false;
}
Boolean verify = JwtUtil.isVerify(claims, user);
if (!verify) {
setResultJson(response, HttpStatus.USER_INFO_ERROR.getCode(), HttpStatus.USER_INFO_ERROR.getTitle());
return false;
}
request.setAttribute("userInfo", user);
return true;
}
/**
* 设置响应json数据
* @param response
* @param code 状态码
* @param title 返回说明
* @return
*/
private void setResultJson(HttpServletResponse response, Integer code, String title) throws Exception{
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json;charset=utf-8");
PrintWriter writer = response.getWriter();
JSONObject resultJson = new JSONObject();
resultJson.put("code", code);
resultJson.put("count", 0);
resultJson.put("title", title);
writer.append(resultJson.toJSONString());
}
}
3.4 自定义返回状态码
/**
* HTTP 状态码
*/
public enum HttpStatus {
OK(20000, "请求成功"),
SUCCESS(20001, "保存成功"),
DELETE(20004, "删除成功"),
FORBIDDEN(40003, "权限不足"),
NOT_FOUND(40004, "资源未找到"),
USER_NOT_EXIT(40081, "用户不存在"),
TOKEN_ERROR(40082, "签名错误"),
TOKEN_EXPIRED(40083, "Token 过期,请重新登录"),
USER_INFO_ERROR(40084, "账号或密码错误"),
ABNORMAL_ACCESS(40091, "异常访问"),
private Integer code;
private String title;
HttpStatus(Integer code, String title) {
this.code = code;
this.title = title;
}
public Integer getCode() {
return code;
}
public String getTitle() {
return title;
}
}
3.5 Controller
package com.huanda.chetaijitoc.admin.controller.userdb;
import com.huanda.chetaijitoc.admin.annotation.LoginToken;
import com.huanda.chetaijitoc.admin.controller.base.AbstractBaseController;
import com.huanda.chetaijitoc.commons.constant.HttpStatus;
import com.huanda.chetaijitoc.commons.domain.userdb.UserInfo;
import com.huanda.chetaijitoc.commons.dto.AbstractBaseResult;
import com.huanda.chetaijitoc.commons.dto.LoadReturnData;
import com.huanda.chetaijitoc.commons.service.userdb.UserInfoService;
import com.huanda.chetaijitoc.commons.utils.BeanValidator;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.ArrayList;
/**
* 用户基本信息表控制器
*/
@RestController
@RequestMapping(value = "users")
public class UserInfoController extends AbstractBaseController<UserInfo> {
@Autowired
UserInfoService userInfoService;
/**
* 登录
* @param userInfo 用户信息
* @return
*/
@PostMapping(value = "login")
@LoginToken
public AbstractBaseResult login(@RequestBody UserInfo userInfo) {
LoadReturnData<UserInfo> loadReturnData = new LoadReturnData<>();
loadReturnData = userInfoService.getByUsername(loadReturnData, userInfo);
return result(loadReturnData.getCode(), loadReturnData.getMsg(), loadReturnData.getToken());
}
/**
* 注册
* @param userInfo 用户信息
* @return
*/
@LoginToken
@PostMapping(value = "register")
public AbstractBaseResult register(@RequestBody UserInfo userInfo) {
// 数据校验
String message = BeanValidator.validator(userInfo);
if(StringUtils.isNotBlank(message)) {
return result(HttpStatus.MISSING_PARAMS.getCode(), 0, message, null);
}
LoadReturnData<UserInfo> loadReturnData = new LoadReturnData<>();
loadReturnData = userInfoService.registe(loadReturnData, userInfo);
return result(loadReturnData.getCode(), 0, loadReturnData.getMsg(), null);
}
@CheckToken
@PostMapping(value = "/test")
public AbstractBaseResult test() {
return result(HttpStatus.OK.getCode(), 0, HttpStatus.OK.getTitle(), null);
}
}
注:上述 AbstractBaseResult(统一返回结果)、LoadReturnData、BeanValidator 和 AbstractBaseController 是自己封装的,这里可以暂不理会,用自己的写的返回即可。
3.6 UserInfoserviceImpl
/**
* 通过用户名获取用户信息并设置 token
* @param loadReturnData 承载数据模型
* @param userInfo 登录用户信息
* @return
*/
@Override
public LoadReturnData<UserInfo> getByUsername(LoadReturnData<UserInfo> loadReturnData, UserInfo userInfo){
Example example = new Example(UserInfo.class);
example.createCriteria().andEqualTo("username", userInfo.getUsername());
UserInfo userDB = userInfoMapper.selectOneByExample(example);
if(userDB == null) {
loadReturnData.setMsg(HttpStatus.USER_NOT_EXIT.getTitle());
loadReturnData.setCode(HttpStatus.USER_NOT_EXIT.getCode());
return loadReturnData;
}
if(!userDB.getPassword().equals(userInfo.getPassword())) {
loadReturnData.setMsg(HttpStatus.USER_INFO_ERROR.getTitle());
loadReturnData.setCode(HttpStatus.USER_INFO_ERROR.getCode());
return loadReturnData;
}
loadReturnData.setCode(HttpStatus.OK.getCode());
loadReturnData.setMsg("登录成功");
loadReturnData.setToken(JwtUtil.createJWT(userDB));
return loadReturnData;
}
3.7 Token 校验测试
- 登录测试
- 需要校验 token
至此,springboot 整合 JWT 基本完成。