git地址: github.com/cladecs/JWT…
簡介
大家以前都使用過session儲存資訊,有的交給容器建立,有的儲存到mysql或者redis,這次專案用到了JWT,我們把使用者的資訊和登入的過期時間都封裝到一個token字串裡,客戶端每次請求只需要在頭資訊裡攜帶token即可,話不多說,下面是目錄結構.
一.annonation註解
package com.demo.annotation;
import java.lang.annotation.*;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface IgnoreLogin {
}
複製程式碼
該註解主要作用是過濾掉請求攔截器,使用該註解就不會對該請求進行攔截(許可權校驗),具體使用下面講.
package com.demo.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 登入使用者資訊
*/
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface LoginUser {}
複製程式碼
該註解作用是SpringMVC引數解析器,類似於RequestBody註解(希望大家瞭解springmvc的引數解析機制),和我們後面的resolver相關聯.
二.bean實體類
package com.demo.bean;
public class User {
private long userId;
private String userName;
private String password;
忽略get/set
}
複製程式碼
我們的使用者資訊
package com.demo.bean;
public class Business {
private String str;
private int num;
忽略get/set
}
複製程式碼
我們的業務引數
三.config配置資訊
package com.demo.config;
import com.demo.interceptor.AuthorizationInterceptor;
import com.demo.resolver.UserArgumentResolver;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import java.util.List;
/**
* MVC配置
*/
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Autowired
private AuthorizationInterceptor authorizationInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(authorizationInterceptor).addPathPatterns("/**");
//注入我們自定義的攔截器,攔截所有請求
}
@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
resolvers.add(new UserArgumentResolver());
//注入我們的使用者引數解析器
}
}
複製程式碼
四.controller
package com.demo.controller;
import com.demo.annotation.IgnoreLogin;
import com.demo.annotation.LoginUser;
import com.demo.bean.Business;
import com.demo.bean.User;
import com.demo.util.JwtUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
@RestController
public class UserController {
private Logger logger = LoggerFactory.getLogger(getClass());
@Autowired
private JwtUtils jwtUtils;
@PostMapping(value = "/login")
@IgnoreLogin
public String login() {
//在此 我們不做登入檢驗 假設檢驗成功
User user = new User();
user.setUserId(9527);
user.setUserName("小星星");
return jwtUtils.generateToken(user);//這裡只是為了測試只返回token,(請求不含IgnoreLogin註解時需要將token放在頭資訊裡)
}
@PostMapping("/business")
public User business(@RequestBody Business business, @LoginUser User user) {//在業務邏輯可以使用註解將我們的user注入進來
logger.info("使用者資訊引數id:{},姓名:{}", user.getUserId(), user.getUserName());
logger.info("我們的業務引數:{},{}", business.getStr(), business.getNum());
return user;
}
}
複製程式碼
可以看到當我們登陸成功後我們可以生成一個token字串返回給客戶端,這個字串包含了使用者資訊和時間資訊(jwt機制),同時我們做了一個模仿業務的請求,business是我們的業務引數,user是我們根據客戶端上發的token解析出來的,下面會講到如何解析.可以看到只要我們需要user的引數,我們就可以直接使用LoginUser註解和User就可以直接得到,非常方便,客戶端並不需要將我們的使用者資訊參雜到我們的業務引數中.相對安全。
五.exception
package com.demo.exception;
public class RRException extends RuntimeException {
private static final long serialVersionUID = 1L;
private String msg;
private int code = 500;
}
複製程式碼
這裡我就不解析了,根據需求可以和客戶端協商相應的錯誤碼
六.interceptor攔截器
package com.demo.interceptor;
import com.demo.annotation.IgnoreLogin;
import com.demo.exception.RRException;
import com.demo.util.JwtUtils;
import io.jsonwebtoken.Claims;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* 許可權(Token)驗證
*/
@Component
public class AuthorizationInterceptor extends HandlerInterceptorAdapter {
@Autowired
private JwtUtils jwtUtils;
public static final String USER_KEY = "user";
private Logger logger = LoggerFactory.getLogger(getClass());
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
if (!(handler instanceof HandlerMethod) || ((HandlerMethod) handler).
getMethodAnnotation(IgnoreLogin.class) != null) {
//如果不是HandlerMethod或者忽略登入
logger.info("無需token校驗,handler:{}", handler);
return true;
}
//獲取使用者憑證
String token = request.getHeader(jwtUtils.getHeader());
if (StringUtils.isBlank(token)) {
token = request.getParameter(jwtUtils.getHeader());
}
//憑證為空
if (StringUtils.isBlank(token)) {
throw new RRException(jwtUtils.getHeader() + "不能為空", HttpStatus.UNAUTHORIZED.value());
}
Claims claims = jwtUtils.getClaimByToken(token);
if (claims == null || jwtUtils.isTokenExpired(claims.getExpiration())) {
throw new RRException(jwtUtils.getHeader() + "失效,請重新登入", HttpStatus.UNAUTHORIZED.value());
}
//設定userId到request裡,後續根據userId,獲取使用者資訊
request.setAttribute(USER_KEY, jwtUtils.getUser(claims));
return true;
}
}
複製程式碼
我們會過濾掉不是HandlerMethod的請求和帶有IgnoreLogin的註解(並不是所有方法都需要校驗,例如登入請求,支付回撥請求),我們會取出客戶端發出的token,解析出來並判斷是否過期,沒有token或者已過期我們可以需要返回一個錯誤碼給客戶端然後重新登入,當我們校驗成功後我們會取出使用者資訊放入到request裡(後面會在引數解析器裡解析出來),這也是這個攔截器的精髓,既能校驗又能獲取使用者的資訊.
七.resolver引數解析器
package com.demo.resolver;
import com.demo.annotation.LoginUser;
import com.demo.bean.User;
import com.demo.interceptor.AuthorizationInterceptor;
import org.springframework.core.MethodParameter;
import org.springframework.web.bind.support.WebDataBinderFactory;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.method.support.ModelAndViewContainer;
import javax.servlet.http.HttpServletRequest;
/**
* 使用者引數解析器
*/
public class UserArgumentResolver implements HandlerMethodArgumentResolver {
@Override
public boolean supportsParameter(MethodParameter parameter) {
return parameter.hasParameterAnnotation(LoginUser.class);
}
@Override
public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
HttpServletRequest request = webRequest.getNativeRequest(HttpServletRequest.class);
User user = (User) request.getAttribute(AuthorizationInterceptor.USER_KEY);
return user;
}
}
複製程式碼
springmvc的引數解析器,需要繼承HandlerMethodArgumentResolver,有兩個方法,第一個就是支援什麼型別的引數,可以看到我們支援擁有LoginUser註解的引數,第二個方法是從request裡取出我們在攔截器中放入的user並返回,這樣就實現了user物件的注入.
八.JwtUtils
package com.demo.util;
import com.alibaba.fastjson.JSONObject;
import com.demo.bean.User;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
/**
* jwt工具類
*/
@Component
@ConfigurationProperties(prefix = "jwt")
public class JwtUtils {
private Logger logger = LoggerFactory.getLogger(getClass());
private long expire;
private String secret;
private String header;
/**
* 生成jwt token
*/
public String generateToken(User user) {
Date nowDate = new Date();
//過期時間
Date expireDate = new Date(nowDate.getTime() + expire * 1000);
return Jwts.builder()
.setHeaderParam("typ", "JWT")
.setSubject(JSONObject.toJSONString(user))
.setIssuedAt(nowDate)
.setExpiration(expireDate)
.signWith(SignatureAlgorithm.HS512, secret)
.compact();
}
/**
* 解析出來claim
* @param token
* @return
*/
public Claims getClaimByToken(String token) {
try {
return Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody();
} catch (Exception e) {
logger.debug("validate is token error ", e);
return null;
}
}
/**
* 得到user
* @param claims
* @return
*/
public User getUser(Claims claims) {
return JSONObject.parseObject(claims.getSubject(), User.class);
}
/**
* token是否過期
* @return true:過期
*/
public boolean isTokenExpired(Date expiration) {
return expiration.before(new Date());
}
public String getSecret() {
return secret;
}
public void setSecret(String secret) {
this.secret = secret;
}
public long getExpire() {
return expire;
}
public void setExpire(long expire) {
this.expire = expire;
}
public String getHeader() {
return header;
}
public void setHeader(String header) {
this.header = header;
}
}
複製程式碼
expire過期時間,secret金鑰,header頭資訊名稱 這些資料在application.yml裡,這裡我們會根據User物件生成一個token字串,根據token取出claims物件,這裡就包含了我們的過期時間和之前我們所存的user資訊.
九.springboot啟動和yml引數
package com.demo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class SpringBootStart {
public static void main(String[] agrs) {
SpringApplication.run(SpringBootStart.class, agrs);
}
}
application.yml
jwt:
#加密祕鑰
secret: f4e2e5203fg45sf45g4de581c0f9eb5
#token,單位秒
expire: 6000
header: token
複製程式碼
十.總結
程式碼隨少,五臟俱全,在這裡我們梳理一下流程.
1:使用者上發登入請求,我們會返回一個token(包含校驗資訊和使用者資訊).
2:客戶端上發請求需要攜帶token到頭資訊裡伺服器需要驗證.
3:伺服器攔截請求取出token並進行解析,把user資訊存入到request中.
4:在springmvc我們增加了引數解析器,將user從request取出並返回,這時候就完成了引數的解析和注入.
5:可以在controller邏輯程式碼使用我們的user物件了.
第一次寫,表達不清楚望大家見諒...