0x00 JWT(JSON Web Token)
JWT的全称是JSON Web Token,它是一种紧凑的、URL安全的,在两方传输之中提供数据的Token。
在用户成功的进行认证之后,可以使用用户信息来生成JWT,并将JWT设置于响应的cookie或者响应头中,在接下来的请求中使用cookie(一般用于浏览器)或者请求头(一般用于App)中的JWT来检查用户的登录状态以及权限信息。由于JWT的特点,我们认为一个有效的JWT中的信息是可信赖的。
0x01 JWT整合入Spring Boot
a.添加依赖
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
jjwt是Java生成和解析JWT的库,当然也可以自己实现。
b.编写过滤器组件
/**
* 如果请求中(请求头或者Cookie)中存在JWT,则:
* 1、解析JWT并查找对应的用户信息,然后加入request attribute中
* 2、更新Cookie时间、更新JWT失效时间放入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();
//获取相应的用户信息,可以在过滤器中先行获取,也可以先保存用户ID,在需要时进行获取
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");
//再从cookie中获取
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工具类
本类是生成和验证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.注册过滤器
将特定的URL或者任意的URL注册过滤器
@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.登录方法
登录之后,在回应头中和cookie中设置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写入JWT中以便在后续过滤中进行解析和用户的查询。用户的查询如果是在集群各机器都能访问到的地方,那么使用JWT验证的方式就天然地支持了分布式的部署,同样的JWT请求到不同的机器上的效果完全是一样的。
0x02 添加注解控制用户访问权限
a.自定义权限控制注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface UserAccess {
/**
* 访问允许的角色,默认不限制角色
*/
String[] value() default "";
/**
* 访问是否需要登录,默认需要登录
*/
boolean needLogin() default true;
}
注解定义在方法上(Controller方法上),通过指定访问需要的角色名称来控制权限
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);
//如果是不需要登录就可以访问的API,则直接放行
if (!userAccess.needLogin()) {
return;
}
HttpServletRequest request =
((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest();
User user = (User) request.getAttribute(WebSecurityFilter.SECURITY_USER);
//需要登录才能访问的API,没有发现用户,返回禁止
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定义的标准注解,可以和其他的库或者别人的代码进行无缝对接,比如Spring Security也实现在@RolesAllowed注解,对注解的方法进行了处理。
切面方法会在加入了自定义注解的方法执行前进行执行,检查用户的权限是否匹配。
网友评论