SpringSecurity5 (4) ——整合jwt
阿新 • • 發佈:2020-07-24
JWT
是一種用於雙方之間傳遞安全資訊的簡潔的、URL安全的表述性宣告規範。JWT
作為一個開放的標準(RFC 7519),定義了一種簡潔的,自包含的方法用於通訊雙方之間以json
物件的形式安全的傳遞資訊。因為數字簽名的存在,這些資訊是可信的,JWT
可以使用HMAC
演算法或者是RSA
的公私祕鑰對進行簽名。
JWT
主要包含三個部分之間用英語句號'.'隔開
- Header 頭部
- Payload 負載
- Signature 簽名
注意,順序是 header.payload.signature
目前系統開發經常使用前後端分離的模式,前端使用vue
等框架,呼叫後端的rest介面返回json
格式的資料,並在前端做展示。登入成功後,後臺會向前端返回一個token,前端每次訪問後臺介面時都攜帶令牌(在header中攜帶令牌資訊),後臺對令牌資訊進行校驗,如果校驗成功可訪問後臺介面。
(一)實現思路
- 我們使用
auth0
的java-jwt
是一個JSON WEB TOKEN(JWT)
的一個實現; - 登入認證成功後,根據一定的規則生成token,並把token返回給前端;
- 增加一個過濾器,對每次請求進行過濾(除登入請求外),檢視請求頭是否攜帶有token資訊,如果攜帶有token資訊,則對token進行驗證,驗證通過則進行下一步,驗證不通過則返回相應異常資訊;前端根據異常資訊做出操作。
(二)具體步驟
1、引入依賴
引入com.auth0
的依賴,用來生成token資訊
<dependency> <groupId>com.auth0</groupId> <artifactId>java-jwt</artifactId> <version>3.4.1</version> </dependency>
2、生成token
在UserDetailsService
的實現類裡增加生成token的方法
/** * 儲存使用者資訊 * @param userDetails */ public User saveUserLoginInfo(UserDetails userDetails) { /** 獲取使用者資訊,此處可修改為從redis中獲取使用者資訊 */ User user = userMapper.getUserByName(userDetails.getUsername()); if (user != null) { String salt = user.getSalt(); Date loginTime = user.getLastLogin(); // 挑出部分使用者資訊,生成token User tokenUser = new User(); tokenUser.setId(user.getId()); tokenUser.setName(user.getName()); // 如果需要重複登陸保持 token boolean useOldToken = false; // 需要重複登陸保持 token,但沒有登陸過,或上次登陸已過期,生成新的 salt 與 loginTime,生成新的 token if (!useOldToken) { salt = BCrypt.gensalt(); loginTime = new Date(); user.setSalt(salt); user.setLastLogin(loginTime); userMapper.updateSalt(user.getId(), salt); } // 生成token Algorithm algorithm = Algorithm.HMAC256(salt); Date expiresTime = expiresTime(loginTime); //使用jwt的API生成token String token = JWT.create() //面向使用者的值 .withSubject(JsonUtil.toJson(tokenUser)). //過期時間 withExpiresAt(expiresTime) //簽發時間 .withIssuedAt(loginTime) //簽名演算法 .sign(algorithm); log.info("JWT Token is generated at {} for user {}, and will be expired at {}", df.format(loginTime), user.getName(), df.format(expiresTime)); // 新增或更新快取 可在此處把使用者資訊更新或新增到快取中 user.setToken(token); return user; } return null; } private Date expiresTime(Date time) { Calendar expiresTime = Calendar.getInstance(); expiresTime.setTime(time); expiresTime.add(Calendar.SECOND, 3600); return expiresTime.getTime(); }
3、增加過濾器、token校驗
新開發一個過濾器,對請求進行攔截並驗證token,如果token沒問題,則放行,如果token異常則返回異常資訊給前端。
新增加token校驗的服務,對token進行解析及驗證是否有效、是否過期。
public class JWTFilter extends OncePerRequestFilter {
private RequestMatcher requiresAuthenticationRequestMatcher;
private List<RequestMatcher> permissiveRequestMatchers;
private TokenService tokenService;
private SecurityUserDetailsService userMapper;
private AuthenticationSuccessHandler successHandler ;
private AuthenticationFailureHandler failureHandler = new AuthFailureHandler();
public JWTFilter(TokenService tokenService, SecurityUserDetailsService userMapper) {
this.requiresAuthenticationRequestMatcher =
new RequestHeaderRequestMatcher("Authorization");
this.tokenService=tokenService;
this.userMapper=userMapper;
}
@Override
public void afterPropertiesSet() {
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
if (!requiresAuthentication(request, response)) {
filterChain.doFilter(request, response);
return;
}
String uri = request.getRequestURI();
// 不攔截登陸
if ( "/login".equals(uri)) {
filterChain.doFilter(request, response);
return;
}
// 從請求頭中獲取token
String token = request.getHeader("Authorization");
//把token解析為jwt物件
DecodedJWT jwt = tokenService.decode(token);
//從資料庫或快取中獲取user物件
User user =tokenService.retrieve(jwt);
// 退出時只檢驗 token 的合法性,是否能解析出來user物件
if ("/logout".equals(uri)) {
try {
tokenService.analytic(token);
// 上下文中快取使用者
} catch (Exception e) {
unsuccessfulAuthentication(request, response, new InternalAuthenticationServiceException("", e));
}
filterChain.doFilter(request, response);
return;
}
//查詢使用者許可權生成Authentication物件,這裡直接寫靜態程式碼,專案中需要從db中查詢使用者相應的角色
List<GrantedAuthority> authorities = new ArrayList<>();
authorities.add(new SimpleGrantedAuthority("ROLE_USER"));
authorities.add(new SimpleGrantedAuthority("ROLE_ROOT"));
Authentication authResult = new UsernamePasswordAuthenticationToken(user.getName(),user.getPassword(),authorities);
AuthenticationException failed = null;
try {
tokenService.validate(token);
} catch (Exception e) {
failed = new InternalAuthenticationServiceException("", e);
}
if (failed == null) {
successfulAuthentication(request, response, filterChain, authResult,token);
} else if (!permissiveRequest(request)) {
unsuccessfulAuthentication(request, response, failed);
return;
}
filterChain.doFilter(request, response);
}
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException {
// token 校驗失敗
failureHandler.onAuthenticationFailure(request, response, failed);
}
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult,String token) throws IOException, ServletException {
/**
*驗證成功可以根據業務需求做一系列操作後,請求繼續往下進
* 比如把使用者資訊放入threadlocal中,供後續操作使用1
*/
SecurityContextHolder.getContext().setAuthentication(authResult);
//根據使用者名稱查詢user物件
//獲取token
DecodedJWT jwt = tokenService.decode(token);
//判斷是否應該重新整理token
if(shouldTokenRefresh(jwt.getExpiresAt())){
User user =userMapper.saveUserLoginInfo((UserDetails) authResult.getPrincipal());
String newToken =user.getToken();
response.setHeader("Authorization", newToken);
}
}
protected boolean requiresAuthentication(HttpServletRequest request, HttpServletResponse response) {
return requiresAuthenticationRequestMatcher.matches(request);
}
protected boolean permissiveRequest(HttpServletRequest request) {
if (permissiveRequestMatchers == null) {
return false;
}
for (RequestMatcher permissiveMatcher : permissiveRequestMatchers) {
if (permissiveMatcher.matches(request)) {
return true;
}
}
return false;
}
/**
* 判斷是否應該重新整理token
* @param expireAt
* @return
*/
protected boolean shouldTokenRefresh(Date expireAt) {
LocalDateTime expireTime = LocalDateTime.ofInstant(expireAt.toInstant(), ZoneId.systemDefault());
LocalDateTime freshTime = expireTime.minusSeconds(1200);
// log.info("Check token refresh, token will expire at {}, need refresh after {}", expireTime.format(dtf), freshTime.format(dtf));
return LocalDateTime.now().isAfter(freshTime);
}
@Component
public class TokenService {
@Autowired
private UserMapper userMapper;
private Logger log = LoggerFactory.getLogger(TokenService.class);
/**
* 只單純解碼 token,取出其中的使用者資訊
*
* @param token
* @return
*/
public DecodedJWT decode(String token) {
if (token == null) {
throw new RuntimeException("使用者未驗證");
}
DecodedJWT jwt = null;
try {
jwt = JWT.decode(token);
} catch (JWTDecodeException e1) {
log.warn("Jwt decode token failed, msg is: {}", e1.getLocalizedMessage());
throw new RuntimeException("token解析錯誤");
}
return jwt;
}
/**
* 從 jwt 中解析出使用者資訊
*
* @param token
* @return
*/
public User analytic(String token) {
return analytic(decode(token));
}
/**
* 從 jwt 中解析出使用者資訊
*
* @param jwt
* @return
*/
public User analytic(DecodedJWT jwt) {
User user = null;
try {
user = JsonUtil.toObject(jwt.getSubject(), User.class);
} catch (Exception e) {
log.warn("Jwt subject convert to User failed, msg is: {}", e.getLocalizedMessage());
throw new RuntimeException("使用者未認證");
}
return user;
}
/**
* 解碼 token,並從快取中或者資料庫中取回使用者的詳細資訊
* @param jwt
* @return
*/
public User retrieve(DecodedJWT jwt) {
User user = null;
try {
user = userMapper.getUserByName(analytic(jwt).getName());
} catch (Exception e) {
log.warn("Retrieve user from redis cache failed, msg is: {}", e.getLocalizedMessage());
}
if (user == null) {
throw new RuntimeException("使用者未登入");
}
return user;
}
/**
* 校驗 token 是否合法
* @param token
* @return
*/
public void validate(String token) {
validate(decode(token), null);
}
/**
* 校驗 token 是否合法
*
* @param jwt
* @param cofUser 從快取中可以取得
* @return
*/
public void validate(DecodedJWT jwt, User user) {
// 是否超時
if (Calendar.getInstance().getTime().after(jwt.getExpiresAt())) {
throw new RuntimeException("token驗證失敗");
}
// 取使用者
if (user == null) {
user = retrieve(jwt);
}
if (user == null) {
throw new RuntimeException("token驗證失敗");
}
// 使用者中不含 salt
if (user.getSalt() == null) {
throw new RuntimeException("token驗證失敗");
}
// 校驗使用者狀態, 只有為 enabled 的使用者才允許登陸
if ("ENABLED".equals(user.getStatus())) {
throw new RuntimeException("token驗證失敗");
}
// 校驗token是否合法
String encryptSalt = user.getSalt();
try {
Algorithm algorithm = Algorithm.HMAC256(encryptSalt);
JWTVerifier verifier = JWT.require(algorithm).withSubject(jwt.getSubject()).build();
verifier.verify(jwt.getToken());
} catch (Exception e) {
log.warn("Jwt verifier token failed, msg is: {}", e.getLocalizedMessage());
throw new RuntimeException("token驗證失敗");
}
}
}
4、修改配置類
@Override
protected void configure(HttpSecurity http) throws Exception {
http.addFilterBefore(imageCodeFilter, UsernamePasswordAuthenticationFilter.class)
.addFilterAfter(getJwtFilter(),ImageCodeFilter.class) //在imageCodeFilter後面加JwtFilter
.authorizeRequests()
.antMatchers("/imageCode").permitAll()
.antMatchers("/hello/admin").hasRole("ROOT")
.antMatchers("/hello").hasRole("USER").anyRequest().permitAll()
.and()
.csrf().disable().
formLogin().loginPage("/login") //自定義登入頁面跳轉
.defaultSuccessUrl("/hello")
.successForwardUrl("/hello/admin")//登入成功後跳轉
.successHandler(authSuccessHandler)
.failureHandler(authFailureHandler)
.and().httpBasic().disable()
.sessionManagement().disable()
.cors()
.and()
.logout().logoutUrl("/logout").addLogoutHandler(authLogoutHandler);
}
/**
* 加密方式 配置對token的驗證過濾器
* @return
*/
@Bean
protected JWTFilter getJwtFilter(){
return new JWTFilter(tokenService,securityUserDetailsService);
}