1. 程式人生 > 實用技巧 >SpringSecurity5 (4) ——整合jwt

SpringSecurity5 (4) ——整合jwt

JWT是一種用於雙方之間傳遞安全資訊的簡潔的、URL安全的表述性宣告規範。JWT作為一個開放的標準(RFC 7519),定義了一種簡潔的,自包含的方法用於通訊雙方之間以json物件的形式安全的傳遞資訊。因為數字簽名的存在,這些資訊是可信的,JWT可以使用HMAC演算法或者是RSA的公私祕鑰對進行簽名。

JWT主要包含三個部分之間用英語句號'.'隔開

  1. Header 頭部
  2. Payload 負載
  3. Signature 簽名

注意,順序是 header.payload.signature

目前系統開發經常使用前後端分離的模式,前端使用vue等框架,呼叫後端的rest介面返回json格式的資料,並在前端做展示。登入成功後,後臺會向前端返回一個token,前端每次訪問後臺介面時都攜帶令牌(在header中攜帶令牌資訊),後臺對令牌資訊進行校驗,如果校驗成功可訪問後臺介面。

(一)實現思路

  1. 我們使用auth0java-jwt是一個JSON WEB TOKEN(JWT)的一個實現;
  2. 登入認證成功後,根據一定的規則生成token,並把token返回給前端;
  3. 增加一個過濾器,對每次請求進行過濾(除登入請求外),檢視請求頭是否攜帶有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);
    }