SpringSecurity+JWT
JWT认证和Session认证
参考资料:什么是 JWT – JSON WEB TOKEN - 简书 (jianshu.com)
JWT:JSON WEB TOKEN
JWT的声明一般被用来在身份提供者和服务提供者间传递被认证的用户身份信息,以便于从资源服务器获取资源,也可以增加一些额外的其它业务逻辑所必须的声明信息,该token也可直接被用于认证,也可被加密。
传统Session认证存在的问题
因为http协议本身是无状态协议,即时用户第一次请求时提供了用户名和密码进行认证,第二次请求时仍需要提供用户名和密码,因为对后端来说并不能知道是哪个用户发出的请求。
传统的Session认证就是用户登录成功后,将用户信息存放在后台session中,前端下次请求时需要在cookie中携带对应的sessionid才能表明其已经登录过。
问题:
-
因为用户信息需要存放在后端的session中(后端内存),当用户信息增多时服务端内存开销变大
-
用户认证后信息保存在当前服务器中,但对于分布式应用来说,其他服务器服务器中是没有该服务器中的session信息的
-
CSRF跨域问题:session认证的结果被存放在cookie中,攻击者是可以在你访问网站时获取到你的cookie并且伪造请求去请求信息
JWT认证方式
token的认证方式下,服务端不需要为用户保存认证信息
大致流程
- 用户账号密码请求服务器
- 服务器认证通过
- 生成一个token(jwt),该jwt是加密的,需要服务端有对于的私钥才能解密。
- 前端存储用户token,并在后续请求头中携带token
- 服务器验证token的值并解密取出token中的信息再次生成security中需要的authentication对象。
具体实现
因为Security的认证流程就是一串和UsernamePasswordFilter类似的过滤器在过滤器链上拦截请求然后认证,认证通过就返回认证信息即可,我们可以模拟UsernamePasswordFilter写一个自己的UsernamePasswordFilter
1. JwtFilter
@Slf4j
@RequiredArgsConstructor
@Component
public class JwtFilter extends OncePerRequestFilter {
private final AppProperties appProperties;
private final JwtUtil jwtUtil;
private final ObjectMapper objectMapper;
/**
主要拦截方法,
这个方法负责对所有进入的请求判断请求头中是否有token
*/
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
var claimsOpt = Optional.ofNullable((Claims) null);
if (checkJwtToken(request)) {
// 如果存在token则对token进行校验
claimsOpt = validateToken(request);
claimsOpt.filter(claims -> Objects.nonNull(claims.get("authorities")))
.ifPresentOrElse(
// 将信息设置到SecurityContext中
this::setSpringAuthentication,
SecurityContextHolder::clearContext
);
}
filterChain.doFilter(request, response);
}
// 判断是否存在token,并且token是否以Bearer 开头
private boolean checkJwtToken(HttpServletRequest request) {
var header = request.getHeader(appProperties.getJwt().getHeader());
return Strings.isNotEmpty(header) && header.startsWith(appProperties.getJwt().getPrefix());
}
/**
* 获取 Token 中的Claims中保存的信息,无则返回 empty()
*/
private Optional<Claims> validateToken(HttpServletRequest request) {
var token = request.getHeader(appProperties.getJwt().getHeader()).replace(appProperties.getJwt().getPrefix(), "");
try {
return Optional.of(Jwts.parserBuilder().setSigningKey(jwtUtil.getKey()).build().parseClaimsJws(token).getBody());
} catch (ExpiredJwtException | SignatureException | MalformedJwtException | UnsupportedJwtException | IllegalArgumentException e) {
return Optional.empty();
}
}
private void setSpringAuthentication(Claims claims) {
// 这里我是直接从token中解析出的用户信息,安全的方式是只在token中存放基本的用户信息,再这里去重新查询用户信息
var rawList = CollectionUtil.convertObjectToList(claims.get("authorities"));
var authorities = rawList.stream()
.map(String::valueOf)
.map(SimpleGrantedAuthority::new)
.collect(Collectors.toList());
var authentication = new UsernamePasswordAuthenticationToken(claims.getSubject(), null, authorities);
SecurityContextHolder.getContext().setAuthentication(authentication);
}
}
2. SecurityConfig配置类
public class BaseSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
protected JwtFilter jwtFilter;
// ....
@Override
protected void configure(HttpSecurity http) throws Exception {
// 这里省略的其他security的配置代码,只给出了如何将filter设置到UsernamePasswordAuthenticationFilter之前的配置
http
// ....
.addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class)
}
}
3. 生成JWT的工具类
pom.xml中导入依赖
<!--JWT-->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.0</version>
</dependency>
创建JwtTokenUtils类
@Slf4j
public class JwtTokenUtils {
public static final String TOKEN_HEADER = "Authorization";
public static final String TOKEN_PREFIX = "Bearer ";
//密钥自定义
private static final String SECRET = "xxx";
//签授人自定义
private static final String ISS = "XXX";
// 过期时间是3600秒,既是1个小时 单位秒
private static final long EXPIRATION = 7200L;
// 选择了记住我之后的过期时间为7天 单位秒
private static final long EXPIRATION_REMEMBER = 604800L;
// 创建token
public static String createToken(String subject,HashMap<String,Object> claims,boolean isRememberMe) {
long expiration = isRememberMe ? EXPIRATION_REMEMBER : EXPIRATION;
return Jwts.builder()
.signWith(SignatureAlgorithm.HS512, SECRET)
.setClaims(claims)
.setIssuer(ISS)
.setSubject(subject)
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + expiration * 1000))
.compact();
}
// 从token中获取用户名
public static String getUsername(String token){
return getTokenBody(token).getSubject();
}
// 是否已过期
public static boolean isExpiration(String token) {
try {
return getTokenBody(token).getExpiration().before(new Date());
} catch (ExpiredJwtException e) {
return true;
}
}
private static Claims getTokenBody(String token){
return Jwts.parser()
.setSigningKey(SECRET)
.parseClaimsJws(token)
.getBody();
}
//解析token并获取我们存放的信息
public static Object getClaims(String token,String claim){
return getTokenBody(token).get(claim);
}
public static String createJwt(User user,Integer rememberMe) throws IOException {
boolean isRemember = rememberMe == 1;
Collection<? extends GrantedAuthority> authorities = user.getAuthorities();
String role = null;
for (GrantedAuthority authority : authorities) {
role = authority.getAuthority();
}
//在token中放入要保存的用户信息
//这里存放的保存的信息就是用户二次请求时我们能获取到的信息
HashMap<String,Object> claims = new HashMap<>();
claims.put("USER_ROLE",role);
claims.put("USER_NAME",user.getUsername());
claims.put("USER_REAL_NAME",user.getRealName());
//创建token并返回
return JwtTokenUtils.createToken(user.getUsername(),claims , isRemember);
}
}