1. JWT简介
为什么要说下呢,JWT三部分组成,就要刚刚笔者参加的2023下半年系统架构师考试中考到了,然后我竟然想不起来了。。。
JWT(JSON Web Token)由三个部分组成,它们分别是头部(header)、载荷(payload)和签名(signature)。
- 头部(Header):JWT的头部是一个包含两个部分的JSON对象,用于描述签名算法和令牌类型。它通常包含以下信息:
typ(类型):令牌的类型,这里通常是"JWT"。
alg(算法):用于签名令牌的算法,例如HMAC、RSA或者其他加密算法。 - 载荷(Payload):JWT的载荷部分是存储实际数据的地方,它包含了一系列声明,也是一个JSON对象。载荷可以包含一些预定义的声明(例如,iss(发行人)、exp(过期时间)、sub(主题)等),以及自定义的声明。这些声明提供了有关令牌的信息,但并没有进行加密。
- 签名(Signature):JWT的签名部分用于验证令牌的真实性和完整性。签名是通过将编码后的头部和载荷与一个密钥进行加密生成的。在验证JWT时,接收方可以使用相同的密钥进行加密,并通过比较签名来确保令牌未被篡改。
签名的生成方式取决于在头部指定的算法。常见的签名算法包括HMAC(使用密钥进行哈希计算)和RSA(使用非对称加密算法)。
最终,这三个部分会用点号(.)连接起来形成一个完整的JWT。例如:头部.Base64编码的头部 + “.” + 载荷.Base64编码的载荷 + “.” + 签名。
2. 新建RequestUtils.java
public class RequestUtils {
/**
* 获取上下文Request
*
* @return
*/
public static HttpServletRequest getRequest() {
return ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
}
/**
* 获取上下文Response
*
* @return
*/
public static HttpServletResponse getResponse() {
return ((ServletRequestAttributes)RequestContextHolder.getRequestAttributes()).getResponse();
}
}
3. 新建Claims.java类
@Data
public class Claims {
/**
* 过期时间
*/
private Long exp;
/**
* 用户ID
*/
private String userId;
/**
* 用户中文名
*/
private String username;
/**
* 用户登录账号
*/
private String account;
/**
* 机构编码
*/
private String orgCode;
/**
* 状态 0启用 1禁用
*/
private String state;
/**
* 用户角色
*/
private List<String> roles;
/**
* 权限列表
*/
private List<String> permissions;
public Claims() {
}
public Claims(Long exp, String username, String account, String state, List<String> roles, List<String> permissions) {
this.exp = exp;
this.username = username;
this.account = account;
this.state = state;
this.roles = roles;
this.permissions = permissions;
}
}
4.新建TokenUtils.java类
public class TokenUtils {
private static final String TOKEN_SING = "Shenjian@Suanfaxiaosheng";
// 过期时间15天
private static final Long EXPIRATION_TIME = 86400000 * 15L;
/**
* 生成Token
*/
public static String buildToken(Claims claims) {
try {
// 对密钥进行签名
byte[] bytes = Base64.encodeBase64(TOKEN_SING.getBytes(), false);
JWSSigner jwsSigner = new MACSigner(Arrays.copyOf(bytes, 128));
// 准备JWS header
JWSHeader jwsHeader = new JWSHeader
.Builder(JWSAlgorithm.HS512)
.type(JOSEObjectType.JWT)
.build();
claims.setExp(new Date(System.currentTimeMillis() + EXPIRATION_TIME).getTime());
Payload payload = new Payload(JSON.toJSONString(claims));
// 封装JWS对象
JWSObject jwsObject = new JWSObject(jwsHeader, payload);
// 签名
jwsObject.sign(jwsSigner);
return "Bearer " + jwsObject.serialize();
} catch (KeyLengthException e) {
e.printStackTrace();
} catch (JOSEException e) {
e.printStackTrace();
}
return null;
}
/**
* 验证token
*
* @param token
* @return
*/
public static boolean validateToken(String token) {
JWSObject jwsObject;
try {
token = token.replace("Bearer ", "");
jwsObject = JWSObject.parse(token);
// HMAC验证器
byte[] decodedBytes = Base64.encodeBase64(TOKEN_SING.getBytes(), false);
JWSVerifier jwsVerifier = new MACVerifier(Arrays.copyOf(decodedBytes, 128));
if (!jwsObject.verify(jwsVerifier)) {
return false;
}
String payload = jwsObject.getPayload().toString();
Claims claims = JSON.parseObject(payload, Claims.class);
if (claims.getExp() < new Date().getTime()) {
return false;
}
return true;
} catch (ParseException | JOSEException e) {
e.printStackTrace();
}
return false;
}
/**
* 从token中获取用户ID
*
* @return
*/
public static String getUserIdFromToken(String token) {
Claims claims = parseToken(token);
return claims.getUserId();
}
public static Claims getClaimsFromToken() {
String token = RequestUtils.getRequest().getHeader("Authorization");
if (StringUtils.isBlank(token)) {
return null;
}
return parseToken(token);
}
/**
* 解析token
*
* @return
*/
private static Claims parseToken(String token) {
JWSObject jwsObject;
try {
token = token.replace("Bearer ", "");
jwsObject = JWSObject.parse(token);
// HMAC验证器
byte[] decodedBytes = Base64.encodeBase64(TOKEN_SING.getBytes(), false);
JWSVerifier jwsVerifier = new MACVerifier(Arrays.copyOf(decodedBytes, 128));
if (!jwsObject.verify(jwsVerifier)) {
return null;
}
String payload = jwsObject.getPayload().toString();
Claims claims = JSON.parseObject(payload, Claims.class);
return claims;
} catch (ParseException | JOSEException e) {
e.printStackTrace();
}
return null;
}
public static void main(String[] args) {
System.out.println(86400000 / 1000 / 60 / 60);
}
}
5.改造login接口
@Service
public class UserServiceImpl implements UserService {
private UserMapper userMapper;
public UserServiceImpl(UserMapper userMapper) {
this.userMapper = userMapper;
}
@Override
public ResponseVo login(UserDto userDto) {
// 根据用户登录名获取用户实体
QueryWrapper<User> wrapper = new QueryWrapper<>();
wrapper.eq("username", userDto.getUsername());
wrapper.last("LIMIT 1");
User user = userMapper.selectOne(wrapper);
// 假设用户一定存在且密码正确
Claims claims = new Claims();
claims.setUserId(user.getId());
claims.setUsername(user.getUsername());
// 获取权限列表
String token = TokenUtils.buildToken(claims);
JSONObject jsonObject = new JSONObject();
jsonObject.put("token", token);
return ResponseVo.success(jsonObject);
}
}
6.效果验证
访问http://localhost:8080/springdoc/swagger-ui/index.html#/%E7%94%A8%E6%88%B7%E7%AE%A1%E7%90%86/login
输入json串{"username": "sfxs","password": 1111}即可成功返回Token

后半文我们将实践前后端访问Token鉴权,后端校验Token的完整代码
1. build.gradle新增依赖包
implementation 'org.springframework.boot:spring-boot-starter-security'
2. 匿名用户无权访问控制
@Slf4j
@Component
public class AnonymousAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException {
// 允许跨域
response.setHeader("Access-Control-Allow-Origin", "*");
// 允许自定义请求头token(允许head跨域)
response.setHeader("Access-Control-Allow-Headers", "Authorization, Role, Accept, Origin, X-Requested-With, Content-Type, Last-Modified");
response.setHeader("Content-type", "application/json;charset=UTF-8");
response.getWriter().print(JSON.toJSONString(ResponseVo.message(ResponseCode.UN_AUTHORIZED)));
}
3. 访问拒绝处理器
@Component
public class MyAccessDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException {
response.getWriter().write(JSON.toJSONString(ResponseVo.message(ResponseCode.LICENSE_EXPIRED)));
}
}
4. 基于数据库登录验证
/**
* 通用实体与DTO互相转换工具类
*/
public class CommonDtoUtils {
public static <T> T transform(Object source, Class<T> targetClass) {
if (source == null) {
return null;
}
try {
String jsonSource = JSON.toJSONString(source);
return JSONObject.parseObject(jsonSource, targetClass);
} catch (Exception ex) {
throw ex;
}
}
public static <T> List<T> transformList(List<?> listSource, Class<T> targetClass) {
if (listSource == null) {
return null;
}
try {
String jsonSource = JSON.toJSONString(listSource);
return JSONArray.parseArray(jsonSource, targetClass);
} catch (Exception ex) {
throw ex;
}
}
}
@Data
public class LoginUserDto implements UserDetails, CredentialsContainer {
/**
* 登录账号
*/
private String username;
/**
* 认证完成后,擦除密码等信息
*/
@Override
public void eraseCredentials() {}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return null;
}
@Override
public String getPassword() {
return null;
}
@Override
public String getUsername() {
return username;
}
/**
* 账户是否未过期,过期无法验证
*/
@Override
public boolean isAccountNonExpired() {
return true;
}
/**
* 指定用户是否解锁,锁定的用户无法进行身份验证
*/
@Override
public boolean isAccountNonLocked() {
return false;
}
/**
* 指示是否已过期的用户的凭据(密码),过期的凭据防止认证
*/
@Override
public boolean isCredentialsNonExpired() {
return false;
}
/**
* 用户是否被启用或禁用。禁用的用户无法进行身份验证。
*/
@Override
public boolean isEnabled() {
return true;
}
}
@Slf4j
@Component
public class DefaultUserDetailsServiceImpl implements UserDetailsService {
private UserMapper userMapper;
@Autowired
private DefaultUserDetailsServiceImpl(UserMapper userMapper) {
this.userMapper = userMapper;
}
@Override
public UserDetails loadUserByUsername(String userId) throws UsernameNotFoundException {
QueryWrapper queryWrapper = new QueryWrapper();
queryWrapper.eq("id", userId);
User userInfo = userMapper.selectOne(queryWrapper);
if (userInfo == null) {
log.info("登录用户账户:{} 不存在", userId);
throw new UsernameNotFoundException("登录用户:" + userId + " 不存在");
}
LoginUserDto loginUserDto = CommonDtoUtils.transform(userInfo, LoginUserDto.class);
return loginUserDto;
}
}
5. 新增Token过滤器
@Slf4j
@Component
public class JwtTokenFilter extends OncePerRequestFilter {
@Resource
private UserDetailsService userDetailsService;
private static final Set<String> ignoreUrlSet = new HashSet<>();
static {
ignoreUrlSet.add("/login");
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws IOException, ServletException {
String token = RequestUtils.getRequest().getHeader("Authorization");
// 过滤登录页面,防止token失效
if (StringUtils.isNotBlank(token) && !"null".equals(token) && !ignoreUrlSet.contains(request.getRequestURI())) {
String userId = TokenUtils.getUserIdFromToken(token);
if (StringUtils.isNotBlank(userId) && SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails userDetails = userDetailsService.loadUserByUsername(userId);
if (TokenUtils.validateToken(token)) {
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
} else {
response.setHeader("Content-type", "application/json;charset=UTF-8");
response.getWriter().print(JSON.toJSONString(ResponseVo.message(ResponseCode.TOKEN_EXPIRATION)));
return ;
}
}
// 权限过滤器,只加载当前菜单下权限
buildCurrentUserRoleByMenuCode(request, response);
}
filterChain.doFilter(request, response);
}
/**
* 获取当前用户当前菜单权限
*
* @param request
* @return
*/
private void buildCurrentUserRoleByMenuCode(HttpServletRequest request, HttpServletResponse response) {
}
}
6. 新增Spring Security 配置,建造者模式
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfiguration {
// 匿名登录处理
private AnonymousAuthenticationEntryPoint anonymousAuthenticationEntryPoint;
private MyAccessDeniedHandler myAccessDeniedHandler;
private JwtTokenFilter jwtTokenFilter;
@Autowired
public SecurityConfiguration(AnonymousAuthenticationEntryPoint anonymousAuthenticationEntryPoint, MyAccessDeniedHandler myAccessDeniedHandler
, JwtTokenFilter jwtTokenFilter) {
this.anonymousAuthenticationEntryPoint = anonymousAuthenticationEntryPoint;
this.myAccessDeniedHandler = myAccessDeniedHandler;
this.jwtTokenFilter = jwtTokenFilter;
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.cors(withDefaults()).csrf((csrf) -> csrf.disable())
// 因为使用JWT,所以不需要HttpSession
.sessionManagement((sessionManagement -> sessionManagement.sessionCreationPolicy(SessionCreationPolicy.STATELESS)))
.authorizeHttpRequests((authz) -> authz
// OPTIONS请求全部放行
.requestMatchers(HttpMethod.OPTIONS, "/**").permitAll()
// 放行接口
.requestMatchers("/login").permitAll()
.requestMatchers("/register").permitAll()
.requestMatchers("/verifyCode").permitAll()
.requestMatchers("/user/resetPassword").permitAll()
// 放行Swagger页面
.requestMatchers("/springdoc/**").permitAll()
// 所有请求全部需要鉴权认证
.anyRequest().authenticated()
)
// 异常处理(权限拒绝、登录失效等)
.headers((headers) -> headers.frameOptions(frameOptionsConfig -> frameOptionsConfig.disable()))
.exceptionHandling(exceptionHandling -> exceptionHandling
.authenticationEntryPoint(anonymousAuthenticationEntryPoint)
.accessDeniedHandler(myAccessDeniedHandler)
);
// 使用自定义的 Token过滤器 验证请求的Token是否合法
http.addFilterBefore(jwtTokenFilter, UsernamePasswordAuthenticationFilter.class);
http.headers(headers -> headers.cacheControl(cacheControlConfig -> {}));
return http.build();
}
}
7. 验证效果
@FeignClient(value = "cloud", contextId = "cloud")
@Component
public interface CloudClient {
@PostMapping(value = "/testToken", produces = MediaType.APPLICATION_JSON_VALUE)
@Operation(summary = "测试Token", tags = "用户管理", security = { @SecurityRequirement(name = "token")})
ResponseVo testToken(@RequestBody UserDto userDto);
}
@RestController
public class CloudController implements CloudClient {
@Override
public ResponseVo testToken(UserDto userDto) {
return ResponseVo.success("TOKEN测试成功");
}
}
应用启动后,我们在Swagger页面首先登录获取Token,然后设置Token,访问testToken接口,效果如下


欢迎关注公众号算法小生,更多原创等你来
本文介绍了基于JWT的鉴权机制,包括JWT的三部分组成:头部、载荷和签名,以及如何在SpringBoot3和Security6中新建相关工具类和改造login接口以实现鉴权。通过登录接口的演示,展示了JWT的使用和验证过程。
2604

被折叠的 条评论
为什么被折叠?



