1. 什么是JWT?
JWT是 Json Web Token 的缩写。它是基于 RFC 7519 标准定义的一种可以安全传输的 小巧 和 自包含 的JSON对象。由于数据是使用数字签名的,所以是可信任的和安全的。JWT可以使用HMAC算法对secret进行加密或者使用RSA的公钥私钥对来进行签名。
具体参见:https://www.jianshu.com/p/576dbf44b2ae
2. 集成
代码在上一篇博文基础下开展。
上一篇:https://blog.youkuaiyun.com/Yuwen_forJava/article/details/120097694
引入jwt的依赖。
<!--jwt-->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
application.yml增加如下配置:
jwt:
secret: secret
# 过期时间,单位:秒
expiration: 10
token: Authorization
tokenHead: 'Bearer '
然后整一个jwt的工具类,JwtUtil.java
package com.example.demooauth2authorization.util;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
/**
* JwtToken生成的工具类
*/
@Slf4j
@Component
@Data
public class JwtUtil {
private static final String CLAIM_KEY_USERNAME = "sub";
private static final String CLAIM_KEY_CREATED = "created";
@Value("${jwt.secret}")
private String secret;
@Value("${jwt.expiration}")
private Long expiration;
@Value("${jwt.tokenHead}")
private String tokenHead;
/**
* 根据负责生成JWT的token
*/
private String generateToken(Map<String, Object> claims) {
return Jwts.builder()
.setClaims(claims)
.setExpiration(generateExpirationDate())
.signWith(SignatureAlgorithm.HS512, secret)
.compact();
}
/**
* 从token中获取JWT中的负载
*/
private Claims getClaimsFromToken(String token) {
token = token.replace(tokenHead, "");
Claims claims = null;
try {
claims = Jwts.parser()
.setSigningKey(secret)
.parseClaimsJws(token)
.getBody();
} catch (ExpiredJwtException expiredJwtException) {
claims = expiredJwtException.getClaims();
} catch (Exception e) {
log.info("JWT格式验证失败:{}", token);
}
return claims;
}
/**
* 生成token的过期时间
*/
private Date generateExpirationDate() {
return new Date(System.currentTimeMillis() + expiration * 1000);
}
/**
* 从token中获取登录用户名
*/
public String getUserNameFromToken(String token) {
String username;
try {
Claims claims = getClaimsFromToken(token);
username = claims.getSubject();
} catch (Exception e) {
username = null;
}
return username;
}
/**
* 验证token是否还有效
*
* @param token 客户端传入的token
*/
public boolean validateToken(String token) {
boolean result = !isTokenExpired(token);
if (result) {
token = tokenHead + token;
refreshHeadToken(token);
}
return result;
}
/**
* 判断token是否已经失效
*/
private boolean isTokenExpired(String token) {
Date expiredDate = getExpiredDateFromToken(token);
return expiredDate.before(new Date());
}
/**
* 从token中获取过期时间
*/
private Date getExpiredDateFromToken(String token) {
Claims claims = getClaimsFromToken(token);
return claims.getExpiration();
}
/**
* 根据用户信息生成token
*/
public String generateToken(UserDetails userDetails) {
Map<String, Object> claims = new HashMap<>();
claims.put(CLAIM_KEY_USERNAME, userDetails.getUsername());
claims.put(CLAIM_KEY_CREATED, new Date());
return generateToken(claims);
}
/**
* 当原来的token没过期时是可以刷新的
*
* @param oldToken 带tokenHead的token
*/
public String refreshHeadToken(String oldToken) {
if (StringUtils.isEmpty(oldToken)) {
return null;
}
String token = oldToken.substring(tokenHead.length());
if (StringUtils.isEmpty(token)) {
return null;
}
// token校验不通过
Claims claims = getClaimsFromToken(token);
if (claims == null) {
return null;
}
// 如果token已经过期,不支持刷新
if (isTokenExpired(token)) {
return null;
}
// 如果token在有效期一半时间之内刚刷新过,返回原token
if (tokenRefreshJustBefore(token, (int) (expiration / 2))) {
return token;
} else {
claims.put(CLAIM_KEY_CREATED, new Date());
return generateToken(claims);
}
}
/**
* 判断token在指定时间内是否刚刚刷新过
*
* @param token 原token
* @param min 指定时间(分)
*/
private boolean tokenRefreshJustBefore(String token, int min) {
Claims claims = getClaimsFromToken(token);
Date created = claims.get(CLAIM_KEY_CREATED, Date.class);
Date refreshDate = new Date();
// 刷新时间在创建时间的指定时间内(当前时间>创建时间&&当前时间>(创建时间+指定时间))
if (refreshDate.after(created) && refreshDate.getTime() > (created.getTime() + min * 60 * 1000L)) {
return true;
}
return false;
}
}
再新建一个jwt过滤器,拦截请求中是否函数token
JwtValidFilter.java
package com.example.demooauth2authorization.security;
import com.example.demooauth2authorization.util.JwtUtil;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* @author 相柳
* @date 2021/9/9
*/
@Component
public class JwtValidFilter extends OncePerRequestFilter {
@Autowired
private JwtUtil jwtUtil;
@Autowired
private UserDetailsService userDetailsService;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
// 获取token
String token = request.getHeader("Authorization");
if (StringUtils.isBlank(token)) {
filterChain.doFilter(request, response);
return;
}
// 检验token
if (jwtUtil.validateToken(token)) {
UserDetails userDetails = userDetailsService.loadUserByUsername(jwtUtil.getUserNameFromToken(token));
// 校验通过
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(userDetails, userDetails.getPassword(), userDetails.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(authentication);
}
filterChain.doFilter(request, response);
}
}
最后把spring security的session的禁用掉,使用咱们的jwt,新的 WebSecurityConfig.java 如下:
package com.example.demooauth2authorization.security;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
/**
* @author 相柳
* @date 2021/9/4
*/
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private LoginValidProvider loginValidProvider;
@Autowired
private LoginSuccessHandle loginSuccessHandle;
@Autowired
private LoginFailHandle loginFailHandle;
@Autowired
private LogoutHandle logoutHandle;
@Autowired
private JwtValidFilter jwtValidFilter;
@Bean
@Override
protected AuthenticationManager authenticationManager() throws Exception {
return super.authenticationManager();
}
/**
* 指定加密方式
*/
@Bean
public PasswordEncoder passwordEncoder() {
// 使用BCrypt加密密码
return new BCryptPasswordEncoder();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
String toLogin = "/member/toLogin";
String checkPath = "/member/login";
http
// 自定义登录页
.formLogin()
// 登录表单提交的url,即和form的action一致
.loginProcessingUrl(checkPath)
// 访问登录页的url
.loginPage(toLogin)
// 设置登录成功和失败的处理器
.successHandler(loginSuccessHandle)
.failureHandler(loginFailHandle)
// 设置登出操作
.and().logout()
.logoutSuccessHandler(logoutHandle)
.deleteCookies("JSESSIONID").invalidateHttpSession(true)
// 登录请求不需要认证
.and().authorizeRequests().antMatchers(toLogin).permitAll()
// 其他的所有请求都需要认证
.anyRequest().authenticated()
// 关闭csrf防护
.and().csrf().disable()
// 调整为让 Spring Security 不创建和使用 session
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
// 添加jwt认证
.and().addFilterBefore(jwtValidFilter, UsernamePasswordAuthenticationFilter.class)
;
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// 设置自定义认证
auth.authenticationProvider(loginValidProvider);
}
}
还有一处,登录成功后,返回token给前端,修改 LoginSuccessHandle.java
package com.example.demooauth2authorization.security;
import com.example.demooauth2authorization.util.JwtUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.stereotype.Component;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* @author 相柳
* @date 2021/9/6
*/
@Slf4j
@Component
public class LoginSuccessHandle implements AuthenticationSuccessHandler {
@Autowired
private UserDetailsService userDetailsService;
@Autowired
private JwtUtil jwtUtil;
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
log.info("进入登录成功handle");
// 生成token返回
String username = request.getParameter("username");
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
String token = jwtUtil.generateToken(userDetails);
response.getWriter().println(token);
}
}
3.测试
访问 http://127.0.0.1:8080/getAll 跳转登录页。
登录后返回token,
然后使用postman再次访问 http://127.0.0.1:8080/getAll ,并添加token。
可以看到返回信息