簡(jiǎn)介
JSON Web token簡(jiǎn)稱JWT康聂, 是用于對(duì)應(yīng)用程序上的用戶進(jìn)行身份驗(yàn)證的標(biāo)記枯饿。也就是說(shuō), 使用 JWTS 的應(yīng)用程序不再需要保存有關(guān)其用戶的 cookie 或其他session數(shù)據(jù)拘荡。此特性便于可伸縮性, 同時(shí)保證應(yīng)用程序的安全勿锅。
在身份驗(yàn)證過(guò)程中, 當(dāng)用戶使用其憑據(jù)成功登錄時(shí), 將返回 JSON Web token, 并且必須在本地保存 (通常在本地存儲(chǔ)中)荧缘。每當(dāng)用戶要訪問(wèn)受保護(hù)的路由或資源 (端點(diǎn)) 時(shí), 用戶代理(user agent)必須連同請(qǐng)求一起發(fā)送 JWT, 通常在授權(quán)標(biāo)頭中使用Bearer schema皆警。后端服務(wù)器接收到帶有 JWT 的請(qǐng)求時(shí), 首先要做的是驗(yàn)證token。
JWT的格式
JWT就是一個(gè)字符串截粗,經(jīng)過(guò)加密處理與校驗(yàn)處理的字符串信姓,形式為:A.B.C
A由JWT頭部信息header加密得到
B由JWT用到的身份驗(yàn)證信息json數(shù)據(jù)加密得到
C由A和B加密得到,是校驗(yàn)部分
背景
項(xiàng)目需要APP端與服務(wù)端Token驗(yàn)證绸罗。由于服務(wù)端包含pc端服務(wù)意推,且只有APP端服務(wù)需要token驗(yàn)證,增加過(guò)濾器和攔截器配置珊蟀,攔截APP端服務(wù)菊值。
集成
1. Application.yml 配置屬性jwt屬性(secret 加密鹽外驱、expire過(guò)期時(shí)間等)
<!-- JWT驗(yàn)證 https://mvnrepository.com/artifact/io.jsonwebtoken/jjwt -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
jwt:
secret: A0B1C2D3E4F5G6H*********************** # 加密yan
expire: 300000 # tocken 過(guò)期時(shí)間,單位秒
authorised-urls: /app/** # 需要認(rèn)證的url腻窒,多個(gè)URL使用英文逗號(hào),分割
2. 啟動(dòng)時(shí)配置類JwtConfig獲取jwt屬性并注冊(cè)JwtUtil Bean
@Configuration("myJwtConfig")
public class JwtConfig {
// 加密鹽
@Value("${jwt.secret}")
private String secret;
// 過(guò)期時(shí)間
@Value("${jwt.expire}")
private long expire;
// 授權(quán)路徑
@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<String> 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<String, Object> 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<String, Object> claims) {
return Jwts.builder()
.setClaims(claims)
.setIssuedAt(new Date())
// 計(jì)算過(guò)期時(shí)間
.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的過(guò)期時(shí)間
*
* @param token token
* @return 過(guò)期時(shí)間
*/
public Date getExpirationDateFromToken(String token) {
return getClaimsFromToken(token)
.getExpiration();
}
/**
* 判斷token是否過(guò)期
*
* @param token token
* @return 已過(guò)期返回true昵宇,未過(guò)期返回false
*/
private Boolean isTokenExpired(String token) {
Date expiration = getExpirationDateFromToken(token);
return expiration.before(new Date());
}
/**
* 判斷token是否非法
*
* @param token token
* @return 未過(guò)期返回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.設(shè)置用戶信息
// HashMap<String, Object> objectObjectHashMap = new HashMap();
// objectObjectHashMap.put("id", "1");
//
// // 測(cè)試1: 生成token
// String token = jwtOperator.generateToken(objectObjectHashMap);
// // 會(huì)生成類似該字符串的內(nèi)容: eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjEiLCJpYXQiOjE1NjU1ODk4MTcsImV4cCI6MTU2Njc5OTQxN30.27_QgdtTg4SUgxidW6ALHFsZPgMtjCQ4ZYTRmZroKCQ
// System.out.println(token);
//
// // 將我改成上面生成的token!!!
// String someToken = "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjEiLCJpYXQiOjE1NjU1ODk4MTcsImV4cCI6MTU2Njc5OTQxN30.27_QgdtTg4SUgxidW6ALHFsZPgMtjCQ4ZYTRmZroKCQ";
// // 測(cè)試2: 如果能token合法且未過(guò)期儿子,返回true
// Boolean validateToken = jwtOperator.validateToken(someToken);
// System.out.println(validateToken);
//
// // 測(cè)試3: 獲取用戶信息
// Claims claims = jwtOperator.getClaimsFromToken(someToken);
// System.out.println(claims);
//
// // 將我改成你生成的token的第一段(以.為邊界)
// String encodedHeader = "eyJhbGciOiJIUzI1NiJ9";
// // 測(cè)試4: 解密Header
// byte[] header = Base64.decodeBase64(encodedHeader.getBytes());
// System.out.println(new String(header));
//
// // 將我改成你生成的token的第二段(以.為邊界)
// String encodedPayload = "eyJpZCI6IjEiLCJpYXQiOjE1NjU1ODk1NDEsImV4cCI6MTU2Njc5OTE0MX0";
// // 測(cè)試5: 解密Payload
// byte[] payload = Base64.decodeBase64(encodedPayload.getBytes());
// System.out.println(new String(payload));
//
// // 測(cè)試6: 這是一個(gè)被篡改的token瓦哎,因此會(huì)報(bào)異常,說(shuō)明JWT是安全的
// jwtOperator.validateToken("eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjEiLCJpYXQiOjE1NjU1ODk3MzIsImV4cCI6MTU2Njc5OTMzMn0.nDv25ex7XuTlmXgNzGX46LqMZItVFyNHQpmL9UQf-aUx");
// }
}
3. AppLoginController調(diào)用JwtUtil的方法生成Token(可以攜帶一些必要參數(shù)柔逼,如登錄人信息蒋譬,單位信息等)
Map<String, Object> 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. 通過(guò)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();
}
}
/**
* 不需要驗(yàn)證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 {
// 如果不是映射到方法直接通過(guò)
if (!(object instanceof HandlerMethod)) {
return true;
}
HandlerMethod handlerMethod = (HandlerMethod) object;
Method method = handlerMethod.getMethod();
//先檢查方法是否有passtoken注釋愉适,有則跳過(guò)認(rèn)證
if (method.isAnnotationPresent(PassToken.class)) {
PassToken passToken = method.getAnnotation(PassToken.class);
if (passToken.required()) {
return true;
}
}
Class<?> beanType = handlerMethod.getBeanType();
//檢查類是否有passtoken注釋犯助,有則跳過(guò)認(rèn)證
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("無(wú)token,請(qǐng)重新登錄");
printContent(httpServletResponse, ResultEnum.TOKEN_NULL);
return false;
} else {
// 解析數(shù)據(jù)并放入httpServletRequest维咸,以便業(yè)務(wù)使用相關(guān)信息
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已過(guò)期;" + e.getMessage());
printContent(httpServletResponse, ResultEnum.TOKEN_EXPIRED);
return false;
} catch (Exception e) {
// 錯(cuò)誤信息: 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 {
}
/**
* 將錯(cuò)誤信息返回
* @param response 響應(yīng)
* @param resultEnum 返回錯(cuò)誤信息枚舉
*/
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. 通過(guò)自定義攔截器JwtInterceptor 攔截指定請(qǐng)求路徑剂买,解析Token中的數(shù)據(jù)并放入HttpServletRequest
// 解析數(shù)據(jù)并放入httpServletRequest,以便業(yè)務(wù)使用相關(guān)信息
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. 各業(yè)務(wù)獲取HttpServletRequest中的userView來(lái)獲取Token中的登錄人信息
@ResponseBody
@RequestMapping("/getuserinfo")
public ResultVO getUserInfo(HttpServletRequest request) {
UserView userView = (UserView) request.getAttribute("userView");
return ResultUtil.success(userView);
}