简介
JSON Web token简称JWT, 是用于对应用程序上的用户进行身份验证的标记。也就是说, 使用 JWTS 的应用程序不再需要保存有关其用户的 cookie 或其他session数据。此特性便于可伸缩性, 同时保证应用程序的安全。
在身份验证过程中, 当用户使用其凭据成功登录时, 将返回 JSON Web token, 并且必须在本地保存 (通常在本地存储中)。每当用户要访问受保护的路由或资源 (端点) 时, 用户代理(user agent)必须连同请求一起发送 JWT, 通常在授权标头中使用Bearer schema。后端服务器接收到带有 JWT 的请求时, 首先要做的是验证token。
JWT的格式
JWT就是一个字符串,经过加密处理与校验处理的字符串,形式为:A.B.C
A由JWT头部信息header加密得到
B由JWT用到的身份验证信息json数据加密得到
C由A和B加密得到,是校验部分
背景
项目需要APP端与服务端Token验证。由于服务端包含pc端服务,且只有APP端服务需要token验证,增加过滤器和拦截器配置,拦截APP端服务。
集成
1. Application.yml 配置属性jwt属性(secret 加密盐、expire过期时间等)
io.jsonwebtoken
jjwt
0.9.1
jwt:
secret: A0B1C2D3E4F5G6H*********************** # 加密yan
expire: 300000 # tocken 过期时间,单位秒
authorised-urls: /app/** # 需要认证的url,多个URL使用英文逗号,分割
2. 启动时配置类JwtConfig获取jwt属性并注册JwtUtil Bean
@Configuration("myJwtConfig")
public class JwtConfig {
// 加密盐
@Value("${jwt.secret}")
private String secret;
// 过期时间
@Value("${jwt.expire}")
private long expire;
// 授权路径
@Value("${jwt.authorised-urls}")
private String[] authorisedUrls;
@Bean
public JwtUtil jwtUtilBean() {
return new JwtUtil(secret, expire);
}
@Bean
public FilterRegistrationBean basicFilterRegistrationBean() {
FilterRegistrationBean registrationBean = new FilterRegistrationBean();
JwtFilter filter = new JwtFilter(jwtUtilBean(), authorisedUrls);
registrationBean.setFilter(filter);
List urlPatterns = new ArrayList<>();
urlPatterns.add("/app/*"); // 拦截路径
registrationBean.setUrlPatterns(urlPatterns);
return registrationBean;
}
}
public class JwtUtil {
private Long EXPIRATION_TIME;
private String SECRET;
public JwtUtil(String secret, long expire) {
this.EXPIRATION_TIME = expire;
this.SECRET = secret;
}
/**
* 为指定用户生成token
*
* @param claims 用户信息
* @return token
*/
public JSONObject generateToken(Map claims) {
JSONObject json = new JSONObject();
json.put(AppConstants.HEADER_TOKEN, AppConstants.TOKEN_PREFIX + " " + generateJwt(claims));
return json;
}
/**
* 为指定用户生成jwt
* @param claims 用户信息
* @return jwt
*/
public String generateJwt(Map claims) {
return Jwts.builder()
.setClaims(claims)
.setIssuedAt(new Date())
// 计算过期时间
.setExpiration(new Date(System.currentTimeMillis() + this.EXPIRATION_TIME * 1000))
.signWith(SignatureAlgorithm.HS512, SECRET)
.compact();
}
/**
* 从token中获取claim
*/
public Claims getClaimsFromToken(String token) {
System.out.println("token is:" + token);
if (token == null) {
return null;
}
return Jwts.parser()
.setSigningKey(SECRET)
.parseClaimsJws(token.replace(AppConstants.TOKEN_PREFIX, ""))
.getBody();
}
/**
* 获取token的过期时间
*
* @param token token
* @return 过期时间
*/
public Date getExpirationDateFromToken(String token) {
return getClaimsFromToken(token)
.getExpiration();
}
/**
* 判断token是否过期
*
* @param token token
* @return 已过期返回true,未过期返回false
*/
private Boolean isTokenExpired(String token) {
Date expiration = getExpirationDateFromToken(token);
return expiration.before(new Date());
}
/**
* 判断token是否非法
*
* @param token token
* @return 未过期返回true,否则返回false
*/
public Boolean validateToken(String token) {
return !isTokenExpired(token);
}
// public static void main(String[] args) throws Exception {
// // 1. 初始化
// JwtUtil jwtOperator = new JwtUtil("aaabbbcccdddeeefffggghhhiiijjjkkklllmmmnnnooopppqqqrrrsssttt", 1209600L);
//
// // 2.设置用户信息
// HashMap objectObjectHashMap = new HashMap();
// objectObjectHashMap.put("id", "1");
//
// // 测试1: 生成token
// String token = jwtOperator.generateToken(objectObjectHashMap);
// // 会生成类似该字符串的内容: eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjEiLCJpYXQiOjE1NjU1ODk4MTcsImV4cCI6MTU2Njc5OTQxN30.27_QgdtTg4SUgxidW6ALHFsZPgMtjCQ4ZYTRmZroKCQ
// System.out.println(token);
//
// // 将我改成上面生成的token!!!
// String someToken = "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjEiLCJpYXQiOjE1NjU1ODk4MTcsImV4cCI6MTU2Njc5OTQxN30.27_QgdtTg4SUgxidW6ALHFsZPgMtjCQ4ZYTRmZroKCQ";
// // 测试2: 如果能token合法且未过期,返回true
// Boolean validateToken = jwtOperator.validateToken(someToken);
// System.out.println(validateToken);
//
// // 测试3: 获取用户信息
// Claims claims = jwtOperator.getClaimsFromToken(someToken);
// System.out.println(claims);
//
// // 将我改成你生成的token的第一段(以.为边界)
// String encodedHeader = "eyJhbGciOiJIUzI1NiJ9";
// // 测试4: 解密Header
// byte[] header = Base64.decodeBase64(encodedHeader.getBytes());
// System.out.println(new String(header));
//
// // 将我改成你生成的token的第二段(以.为边界)
// String encodedPayload = "eyJpZCI6IjEiLCJpYXQiOjE1NjU1ODk1NDEsImV4cCI6MTU2Njc5OTE0MX0";
// // 测试5: 解密Payload
// byte[] payload = Base64.decodeBase64(encodedPayload.getBytes());
// System.out.println(new String(payload));
//
// // 测试6: 这是一个被篡改的token,因此会报异常,说明JWT是安全的
// jwtOperator.validateToken("eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjEiLCJpYXQiOjE1NjU1ODk3MzIsImV4cCI6MTU2Njc5OTMzMn0.nDv25ex7XuTlmXgNzGX46LqMZItVFyNHQpmL9UQf-aUx");
// }
}
3. AppLoginController调用JwtUtil的方法生成Token(可以携带一些必要参数,如登录人信息,单位信息等)
Map claims = new HashMap<>();
// 部门信息
Organization orgInfo = RightProxy.getPartyDataService().getEmployeeOrg(CommonTool.getPersonPartyId(resultUser.getLoginId()), CommonUtil.getDateTime());
claims.put("loginId", resultUser.getLoginId());
claims.put("loginCode", resultUser.getLoginCode());
claims.put("organizationId", orgInfo.getOrganizationId());
claims.put("personName", resultUser.getPersonName());
JSONObject json = new JSONObject();
json.put(AppConstants.HEADER_TOKEN, AppConstants.TOKEN_PREFIX + " " + jwtUtil.generateJwt(claims));
4. 通过InterceptorConfig配置拦截目录和自定义拦截器JwtInterceptor
@Configuration
public class InterceptorConfig extends WebMvcConfigurerAdapter {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(jwtInterceptor())
.addPathPatterns("/app/**");
}
@Bean
public JwtInterceptor jwtInterceptor() {
return new JwtInterceptor();
}
}
/**
* 不需要验证Token的方法、类
*/
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface PassToken {
boolean required() default true;
}
public class JwtInterceptor implements HandlerInterceptor {
@Autowired
private JwtUtil jwtUtil;
// @Autowired
// UserService userService;
@Override
public boolean preHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object object) throws Exception {
// 如果不是映射到方法直接通过
if (!(object instanceof HandlerMethod)) {
return true;
}
HandlerMethod handlerMethod = (HandlerMethod) object;
Method method = handlerMethod.getMethod();
//先检查方法是否有passtoken注释,有则跳过认证
if (method.isAnnotationPresent(PassToken.class)) {
PassToken passToken = method.getAnnotation(PassToken.class);
if (passToken.required()) {
return true;
}
}
Class> beanType = handlerMethod.getBeanType();
//检查类是否有passtoken注释,有则跳过认证
if (beanType.isAnnotationPresent(PassToken.class)) {
PassToken passToken = beanType.getAnnotation(PassToken.class);
if (passToken.required()) {
return true;
}
}
String token = httpServletRequest.getHeader(AppConstants.HEADER_TOKEN);
try {
if (StringUtils.isBlank(token)) {
LogUtil.getAppLoger().info("无token,请重新登录");
printContent(httpServletResponse, ResultEnum.TOKEN_NULL);
return false;
} else {
// 解析数据并放入httpServletRequest,以便业务使用相关信息
Claims claims = jwtUtil.getClaimsFromToken(token);
UserView userView = new UserView();
// 组织信息
Organization organization = new Organization();
organization.setOrganizationId(Long.valueOf(claims.get("organizationId").toString()));
// 人员信息
UserLogin userLogin = new UserLogin();
userLogin.setLoginId(Long.valueOf(claims.get("loginId").toString()));
userLogin.setLoginCode((String) claims.get("loginCode"));
userLogin.setPersonName((String) claims.get("personName"));
userView.setLoginCode((String) claims.get("loginCode"));
userView.setOrganization(organization);
userView.setUserLogin(userLogin);
userView.setToken(token);
httpServletRequest.setAttribute("userView", userView);
// 获取登录信息
}
} catch (ExpiredJwtException e) {
LogUtil.getAppLoger().info("token已过期;" + e.getMessage());
printContent(httpServletResponse, ResultEnum.TOKEN_EXPIRED);
return false;
} catch (Exception e) {
// 错误信息: httpServletResponse. sendError(HttpServletResponse.SC_UNAUTHORIZED, "token非法");
LogUtil.getAppLoger().info("token非法;" + e.getMessage());
printContent(httpServletResponse, ResultEnum.TOKEN_ILLEGAL);
return false;
}
return true;
}
@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 {
}
/**
* 将错误信息返回
* @param response 响应
* @param resultEnum 返回错误信息枚举
*/
private static void printContent(HttpServletResponse response, ResultEnum resultEnum) {
PrintWriter pw = null;
try {
response.reset();
response.setContentType("application/json");
response.setHeader("Cache-Control", "no-store");
response.setCharacterEncoding("UTF-8");
ResultVO resultVO = ResultUtil.error(resultEnum);
String content = JSON.toJSONString(resultVO);
pw = response.getWriter();
pw.write(content);
pw.flush();
} catch (Exception e) {
LogUtil.getAppLoger().info("拦截器输出流异常;" + e.getMessage());
} finally {
if (pw != null) {
pw.close();
}
}
}
}
5. 通过自定义拦截器JwtInterceptor 拦截指定请求路径,解析Token中的数据并放入HttpServletRequest
// 解析数据并放入httpServletRequest,以便业务使用相关信息
Claims claims = jwtUtil.getClaimsFromToken(token);
UserView userView = new UserView();
// 组织信息
Organization organization = new Organization();
organization.setOrganizationId(Long.valueOf(claims.get("organizationId").toString()));
// 人员信息
UserLogin userLogin = new UserLogin();
userLogin.setLoginId(Long.valueOf(claims.get("loginId").toString()));
userLogin.setLoginCode((String) claims.get("loginCode"));
userLogin.setPersonName((String) claims.get("personName"));
userView.setLoginCode((String) claims.get("loginCode"));
userView.setOrganization(organization);
userView.setUserLogin(userLogin);
userView.setToken(token);
httpServletRequest.setAttribute("userView", userView);
6. 各业务获取HttpServletRequest中的userView来获取Token中的登录人信息
@ResponseBody
@RequestMapping("/getuserinfo")
public ResultVO getUserInfo(HttpServletRequest request) {
UserView userView = (UserView) request.getAttribute("userView");
return ResultUtil.success(userView);
}