0x00 JWT(JSON Web Token)
JWT的全稱是JSON Web Token,它是一種緊湊的、URL安全的,在兩方傳輸之中提供數(shù)據(jù)的Token。
在用戶成功的進(jìn)行認(rèn)證之后,可以使用用戶信息來(lái)生成JWT,并將JWT設(shè)置于響應(yīng)的cookie或者響應(yīng)頭中,在接下來(lái)的請(qǐng)求中使用cookie(一般用于瀏覽器)或者請(qǐng)求頭(一般用于App)中的JWT來(lái)檢查用戶的登錄狀態(tài)以及權(quán)限信息。由于JWT的特點(diǎn),我們認(rèn)為一個(gè)有效的JWT中的信息是可信賴的。
0x01 JWT整合入Spring Boot
a.添加依賴
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
jjwt是Java生成和解析JWT的庫(kù),當(dāng)然也可以自己實(shí)現(xiàn)。
b.編寫(xiě)過(guò)濾器組件
/**
* 如果請(qǐng)求中(請(qǐng)求頭或者Cookie)中存在JWT,則:
* 1、解析JWT并查找對(duì)應(yīng)的用戶信息,然后加入request attribute中
* 2、更新Cookie時(shí)間、更新JWT失效時(shí)間放入Header
*/
@Component
@Slf4j
public class WebSecurityFilter extends OncePerRequestFilter {
public static final String SECURITY_USER = "SECURITY_USER";
@Resource
private JwtUtil jwtUtil;
@Resource
private UserRepository userRepository;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
try {
String jwt = getJwt(request);
if (StringUtils.isNotBlank(jwt) && jwtUtil.validateJwtToken(jwt) != null) {
Jws<Claims> claimsJws = jwtUtil.validateJwtToken(jwt);
String userUid = claimsJws.getBody().getSubject();
//獲取相應(yīng)的用戶信息,可以在過(guò)濾器中先行獲取,也可以先保存用戶ID,在需要時(shí)進(jìn)行獲取
Optional<User> optionalUser = userRepository.findUserByUserUid(userUid);
if (optionalUser.isPresent()) {
request.setAttribute(SECURITY_USER, optionalUser.get());
jwt = "Bearer " + jwtUtil.refreshJwt(jwt);
Cookie jwtCookie = new Cookie("Authorization", URLEncoder.encode(jwt, "UTF-8"));
jwtCookie.setHttpOnly(true);
jwtCookie.setMaxAge(jwtUtil.getJwtExpiration());
jwtCookie.setPath("/");
response.addCookie(jwtCookie);
response.addHeader("Authorization", jwt);
}
}
} catch (Exception e) {
log.error("Can NOT set user authentication -> Message: {}", e);
}
filterChain.doFilter(request, response);
}
private String getJwt(HttpServletRequest request) {
//先從header中獲取
String authHeader = request.getHeader("Authorization");
//再?gòu)腸ookie中獲取
if (StringUtils.isBlank(authHeader)) {
try {
Cookie[] cookies = request.getCookies();
if (cookies != null) {
for (Cookie cookie : cookies) {
if ("Authorization".equals(cookie.getName())) {
authHeader = URLDecoder.decode(cookie.getValue(), "UTF-8");
}
}
}
} catch (Exception e) {
log.error("Can NOT get jwt from cookie -> Message: {}", e);
}
}
if (StringUtils.startsWith(authHeader, "Bearer ")) {
authHeader = authHeader.replace("Bearer ", "");
}
return authHeader;
}
}
c.JWT工具類
本類是生成和驗(yàn)證JWT的方法
@Component
@Slf4j
public class JwtUtil {
@Value("${jwt.secretKey:paycms}")
private String jwtSecret;
@Value("${jwt.expiration:86400}")
private int jwtExpiration;
public int getJwtExpiration() {
return this.jwtExpiration;
}
public static final String CLAIM_KEY_ROLES = "roles";
public String generateJwt(String subject, LocalDateTime localDateTime, Map<String, Object> claims) {
Date issued = Date.from(localDateTime.atZone(ZoneId.systemDefault()).toInstant());
Date expiration = Date.from(localDateTime.plusSeconds(jwtExpiration).atZone(ZoneId.systemDefault()).toInstant());
JwtBuilder jwtBuilder = Jwts.builder()
.setId(UUID.randomUUID().toString())
.setSubject(subject)
.setIssuedAt(issued)
.setExpiration(expiration)
.signWith(SignatureAlgorithm.HS512, jwtSecret);
if (claims.get(CLAIM_KEY_ROLES) != null) {
jwtBuilder.claim(CLAIM_KEY_ROLES, claims.get(CLAIM_KEY_ROLES));
}
return jwtBuilder.compact();
}
public String refreshJwt(String jwt) {
Jws<Claims> claimsJws = validateJwtToken(jwt);
String subject = claimsJws.getBody().getSubject();
Object roles = claimsJws.getBody().get(CLAIM_KEY_ROLES);
Map<String, Object> claims = new HashMap<>();
if (roles != null) {
claims.put(CLAIM_KEY_ROLES, roles);
}
return generateJwt(subject, LocalDateTime.now(), claims);
}
public Jws<Claims> validateJwtToken(String authToken) {
Jws<Claims> claimsJws = null;
if (StringUtils.isNotBlank(authToken)) {
try {
claimsJws = Jwts.parser().setSigningKey(jwtSecret).parseClaimsJws(authToken);
} catch (SignatureException e) {
log.error("Invalid JWT signature -> Message: {} ", e);
} catch (MalformedJwtException e) {
log.error("Invalid JWT token -> Message: {}", e);
} catch (ExpiredJwtException e) {
log.error("Expired JWT token -> Message: {}", e);
} catch (UnsupportedJwtException e) {
log.error("Unsupported JWT token -> Message: {}", e);
} catch (IllegalArgumentException e) {
log.error("JWT claims string is empty -> Message: {}", e);
}
}
return claimsJws;
}
}
d.注冊(cè)過(guò)濾器
將特定的URL或者任意的URL注冊(cè)過(guò)濾器
@Configuration
public class WebSecurityFilterConfig {
@Resource
private WebSecurityFilter webSecurityFilter;
@Bean
public FilterRegistrationBean webSecurityFilterRegistration() {
FilterRegistrationBean<WebSecurityFilter> registration = new FilterRegistrationBean<>();
registration.setFilter(webSecurityFilter);
registration.addUrlPatterns("/api/*");
registration.setOrder(1);
return registration;
}
}
e.登錄方法
登錄之后,在回應(yīng)頭中和cookie中設(shè)置JWT
String jwt = "Bearer " + jwtUtil.generateJwt("get user id from some where", LocalDateTime.now(),
ImmutableMap.of(CLAIM_KEY_ROLES, "some roles"));
Cookie jwtCookie = new Cookie("Authorization", URLEncoder.encode(jwt, "UTF-8"));
jwtCookie.setHttpOnly(true);
jwtCookie.setMaxAge(jwtUtil.getJwtExpiration());
response.addCookie(jwtCookie);
response.addHeader("Authorization", jwt);
將用戶ID寫(xiě)入JWT中以便在后續(xù)過(guò)濾中進(jìn)行解析和用戶的查詢。用戶的查詢?nèi)绻窃诩焊鳈C(jī)器都能訪問(wèn)到的地方,那么使用JWT驗(yàn)證的方式就天然地支持了分布式的部署,同樣的JWT請(qǐng)求到不同的機(jī)器上的效果完全是一樣的。
0x02 添加注解控制用戶訪問(wèn)權(quán)限
a.自定義權(quán)限控制注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface UserAccess {
/**
* 訪問(wèn)允許的角色,默認(rèn)不限制角色
*/
String[] value() default "";
/**
* 訪問(wèn)是否需要登錄,默認(rèn)需要登錄
*/
boolean needLogin() default true;
}
注解定義在方法上(Controller方法上),通過(guò)指定訪問(wèn)需要的角色名稱來(lái)控制權(quán)限
b.加入切面方法(spring aop)
@Aspect
@Component
@Slf4j
public class WebSecurityAspect {
@Before(value = "@annotation(UserAccess)")
public void authoritiesCheck(JoinPoint joinPoint) throws Throwable {
String methodName = joinPoint.getSignature().getName();
log.info("user access security check for method: {}", methodName);
try {
UserAccess userAccess = ((MethodSignature) joinPoint.getSignature()).getMethod().getAnnotation(UserAccess.class);
//如果是不需要登錄就可以訪問(wèn)的API,則直接放行
if (!userAccess.needLogin()) {
return;
}
HttpServletRequest request =
((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest();
User user = (User) request.getAttribute(WebSecurityFilter.SECURITY_USER);
//需要登錄才能訪問(wèn)的API,沒(méi)有發(fā)現(xiàn)用戶,返回禁止
if (user == null) {
throw new ForbiddenAccessException("User Not Found");
}
//value為空直接放行,代表登錄即可
String[] permitRoles = userAccess.value();
if (ArrayUtils.isEmpty(permitRoles)) {
return;
}
if (CollectionUtils.isEmpty(user.getUserRoles())) {
throw new UnauthorizedAccessException("User Has No Authorities");
}
Set<String> roles = user.getUserRoles().stream().map(UserRole::getRoleCode).collect(Collectors.toSet());
Set<String> intersectionRoles =
Stream.of(permitRoles).filter(roles::contains).collect(Collectors.toSet());
if (CollectionUtils.isEmpty(intersectionRoles)) {
throw new UnauthorizedAccessException("User Has No Authorities");
}
} catch (UnauthorizedAccessException | ForbiddenAccessException e) {
throw e;
} catch (Exception e) {
log.error("user access security check Exception: {}", methodName, e);
throw e;
}
}
}
c.注解的使用
直接定義在controller方法上即可,例如
@RequestMapping(value = "/api/protected", method = RequestMethod.GET)
@ResponseBody
@UserAccess(Role.Constants.SYS_ADMIN)
另外,可以使用自定義的@UserAccess注解,也可以使用JSR-250 javax.annotation.security.RolesAllowed 注解,好處是使用了Java定義的標(biāo)準(zhǔn)注解,可以和其他的庫(kù)或者別人的代碼進(jìn)行無(wú)縫對(duì)接,比如Spring Security也實(shí)現(xiàn)在@RolesAllowed注解,對(duì)注解的方法進(jìn)行了處理。
切面方法會(huì)在加入了自定義注解的方法執(zhí)行前進(jìn)行執(zhí)行,檢查用戶的權(quán)限是否匹配。