https://jwt.io/
https://jwt.io/libraries?language=Java
<!-- JJWT(Java JWT) -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
package cn.tedu.csmallpassport;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.junit.jupiter.api.Test;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
public class JwtTest {
String secretKey = "kU4jrFA3iuI5jn25u743kfDs7a8pFEwS54hm"; // 盐值
@Test
public void generate() {
Date date = new Date(System.currentTimeMillis() + 5 * 60 * 1000);
Map<String, Object> claims = new HashMap<>();
claims.put("id", "1233");
claims.put("username", "zhangsan");
String jwt = Jwts.builder()
// Header(头部信息):声明算法与Token的类型
.setHeaderParam("alg", "HS256")
.setHeaderParam("typ", "JWT")
// Payload(载荷):数据,表现为Claims
.setClaims(claims)
.setExpiration(date) // 过期时间
// Signature:验证签名
.signWith(SignatureAlgorithm.HS256, secretKey)
// 完成
.compact();
System.out.println(jwt); // eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjEyMzMiLCJleHAiOjE2ODA3NTA4MzQsInVzZXJuYW1lIjoiemhhbmdzYW4ifQ.7RNj9hWPX8ebFcYrBooFNAlKgc3AIV_WU_qF0wsbltQ
}
@Test
public void parse() {
try {
String jwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjEyMzMiLCJleHAiOjE2ODA3NTA4MzQsInVzZXJuYW1lIjoiemhhbmdzYW4ifQ.7RNj9hWPX8ebFcYrBooFNAlKgc3AIV_WU_qF0wsbltQ";
Claims claims = Jwts.parser()
.setSigningKey(secretKey)
.parseClaimsJws(jwt)
.getBody();
Long id = claims.get("id", Long.class);
String username = claims.get("username", String.class);
System.out.println("id = " + id);
System.out.println("username = " + username);
} catch (Throwable e) {
e.printStackTrace();
}
}
}
在项目中使用JWT识别客户端
验证登录成功后响应JWT
/**
* 验证管理员登录
* @param adminLoginDTO 管理员的登录信息,至少封装用户名与密码原文
* @return 验证登录通过后的JWT
*/
String login(AdminLoginDTO adminLoginDTO);
@Override
public String login(AdminLoginDTO adminLoginDTO) {
log.debug("开始处理【管理员登录】的业务,参数:{}", adminLoginDTO);
// 创建认证信息对象
Authentication authentication = new UsernamePasswordAuthenticationToken(
adminLoginDTO.getUsername(), adminLoginDTO.getPassword());
// 调用认证管理器执行认证
Authentication authenticationResult
= authenticationManager.authenticate(authentication);
log.debug("验证登录成功,返回的Authentication为:{}", authenticationResult);
// 如果没有出现异常,则表示验证登录成功,需要将认证信息存入到Security上下文
// log.debug("即将向SecurityContext中存入Authentication");
// SecurityContext securityContext = SecurityContextHolder.getContext();
// securityContext.setAuthentication(authenticationResult);
// ========== 以下是新增的代码片段 ==========
// 处理验证登录成功后的结果中的当事人
Object principal = authenticationResult.getPrincipal();
log.debug("获取验证登录成功后的结果中的当事人:{}", principal);
AdminDetails adminDetails = (AdminDetails) principal;
// 需要写入到JWT中的数据
Map<String, Object> claims = new HashMap<>();
claims.put("id", adminDetails.getId());
claims.put("username", adminDetails.getUsername());
log.debug("即将生成JWT数据,包含的账号信息:{}", claims);
// 生成JWT,并返回JWT
String secretKey = "kU4jrFA3iuI5jn25u743kfDs7a8pFEwS54hm";
Date exp = new Date(System.currentTimeMillis() + 10 * 24 * 60 * 60 * 1000);
String jwt = Jwts.builder()
.setHeaderParam("alg", "HS256")
.setHeaderParam("typ", "JWT")
.setClaims(claims)
.setExpiration(exp)
.signWith(SignatureAlgorithm.HS256, secretKey)
.compact();
log.debug("生成了JWT数据,并将返回此JWT数据:{}", jwt);
return jwt;
}
@PostMapping("/login")
public JsonResult login(AdminLoginDTO adminLoginDTO) {
log.debug("开始处理【管理员登录】的请求,参数:{}", adminLoginDTO);
String jwt = adminService.login(adminLoginDTO);
return JsonResult.ok(jwt);
}
解析客户端携带的JWT(使用过滤器)
在项目的根包下创建filter.JwtAuthorizationFilter
类,继承自OncePerRequestFilter
抽象类(将间接的实现Filter
接口),并在类上添加组件注解:
package cn.tedu.csmall.passport.filter;
import lombok.extern.slf4j.Slf4j;
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;
@Slf4j
@Component
public class JwtAuthorizationFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(
HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
}
}
在处理过程中,首先,需要尝试接收客户端携带的JWT:
@Slf4j
@Component
public class JwtAuthorizationFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
// 根据业内惯用的做法,客户端应该将JWT保存在请求头(Request Header)中的名为Authorization的属性名
String jwt = httpServletRequest.getHeader("Authorization");
log.debug("尝试接收客户端携带的JWT数据,JWT数据:{}", jwt);
filterChain.doFilter(request, response);
}
}
@Slf4j
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
// 新增代码
@Autowired
private JwtAuthorizationFilter jwtAuthorizationFilter;
// 暂不关心其它代码
}
// 将自定义的JWT过滤器添加在Spring Security的UsernamePasswordAuthenticationFilter之前
http.addFilterBefore(jwtAuthorizationFilter,
UsernamePasswordAuthenticationFilter.class);
package cn.tedu.csmall.passport.filter;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
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;
import java.util.ArrayList;
import java.util.Collection;
/**
* <p>处理JWT的过滤器类</p>
*
* <p>此过滤器类的主要职责:</p>
* <ul>
* <li>尝试接收客户端携带的JWT</li>
* <li>尝试解析接收到的JWT</li>
* <li>将解析成功后得到的结果创建为Authentication并存入到SecurityContext中</li>
* </ul>
*
* @author java@tedu.cn
* @version 0.0.1
*/
@Slf4j
@Component
public class JwtAuthorizationFilter extends OncePerRequestFilter {
public static final int JWT_MIN_LENGTH = 113;
@Override
protected void doFilterInternal(
HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
// 根据业内惯用的做法,客户端应该将JWT保存在请求头(Request Header)中的名为Authorization的属性名
String jwt = request.getHeader("Authorization");
log.debug("尝试接收客户端携带的JWT数据,JWT:{}", jwt);
// 判断客户端是否携带了基本有效的JWT
if (!StringUtils.hasText(jwt) || jwt.length() < JWT_MIN_LENGTH) {
// 客户端没有携带有铲的JWT,则“放行”,交由后续的组件继续处理
filterChain.doFilter(request, response);
// 【重要】终止当前方法的执行,不执行接下来的代码
return;
}
// TODO:1-声明secretKey不合理,应该集中管理
// TODO:2-解析JWT时可能出现异常,需要处理
// 客户端携带了基本有效的JWT,则尝试解析JWT
String secretKey = "kU4jrFA3iuI5jn25u743kfDs7a8pFEwS54hm";
Claims claims = Jwts.parser()
.setSigningKey(secretKey)
.parseClaimsJws(jwt)
.getBody();
Long id = claims.get("id", Long.class);
String username = claims.get("username", String.class);
log.debug("从JWT中解析得到的管理员ID:{}", id);
log.debug("从JWT中解析得到的管理员用户名:{}", username);
// TODO:3-使用用户名的字符串作为“当事人”并不是最优解
// TODO:4-需要调整使用真实的权限
// 基于解析JWT的结果创建Authentication对象
Object principal = username; // 当事人:暂时使用用户名
Object credentials = null; // 凭证:应该为null
Collection<SimpleGrantedAuthority> authorities = new ArrayList<>();
authorities.add(new SimpleGrantedAuthority("暂时放一个山寨的权限"));
Authentication authentication = new UsernamePasswordAuthenticationToken(
principal, credentials, authorities);
// 将Authentication存入到SecurityContext中
log.debug("向SecurityContext中存入Authentication:{}", authentication);
SecurityContext securityContext = SecurityContextHolder.getContext();
securityContext.setAuthentication(authentication);
// 过滤器链继续执行,相当于“放行”
filterChain.doFilter(request, response);
}
}
关于SecurityContext中的认证信息
// 清空SecurityContext,避免【此前携带JWT成功访问后,在接下来的一段时间内不携带JWT也能访问】
SecurityContextHolder.clearContext();
// 将Session策略设置为“从不使用”:STATELESS=无状态,即从不使用Session,NEVER=从不主动创建Session
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
关于当事人
package cn.tedu.csmallpassport.security;
import lombok.Data;
import java.io.Serializable;
@Data
public class LoginPrincipal implements Serializable {
private Long id;
private String username;
}
处理解析JWT时的异常
ERR_JWT_EXPIRED(600, "JWT已过期"),
ERR_JWT_SIGNATURE(601, "验证签名失败"),
ERR_JWT_MALFORMED(602, "JWT格式错误"),
// 客户端携带了基本有效的JWT,则尝试解析JWT
Claims claims = null;
response.setContentType("application/json; charset=utf-8;");
try {
claims = Jwts.parser()
.setSigningKey(secretKey)
.parseClaimsJws(jwt)
.getBody();
} catch (ExpiredJwtException e) {
String message = "您的登录信息已过期,请重新登录!";
log.warn(message);
JsonResult jsonResult = JsonResult.fail(ServiceCode.ERR_JWT_EXPIRED, message);
String jsonResultString = JSON.toJSONString(jsonResult);
PrintWriter writer = response.getWriter();
writer.println(jsonResultString);
writer.close();
return;
} catch (SignatureException e) {
String message = "非法访问!";
log.warn(message);
JsonResult jsonResult = JsonResult.fail(ServiceCode.ERR_JWT_SIGNATURE, message);
String jsonResultString = JSON.toJSONString(jsonResult);
PrintWriter writer = response.getWriter();
writer.println(jsonResultString);
writer.close();
return;
} catch (MalformedJwtException e) {
String message = "非法访问!";
log.warn(message);
JsonResult jsonResult = JsonResult.fail(ServiceCode.ERR_JWT_MALFORMED, message);
String jsonResultString = JSON.toJSONString(jsonResult);
PrintWriter writer = response.getWriter();
writer.println(jsonResultString);
writer.close();
return;
} catch (Throwable e) {
String message = "服务器忙,请稍后再次尝试!(开发过程中,如果看到此提示,请检查控制台的信息,并在JWT过滤器补充处理此异常)";
log.warn(message);
JsonResult jsonResult = JsonResult.fail(ServiceCode.ERR_UNKNOWN, message);
String jsonResultString = JSON.toJSONString(jsonResult);
PrintWriter writer = response.getWriter();
writer.println(jsonResultString);
writer.close();
}
处理未登录的错误
// 处理“当客户端提交请求时没有携带JWT,请求的目标却是需要通过认证的资源”的问题
http.exceptionHandling().authenticationEntryPoint(new AuthenticationEntryPoint() {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException {
response.setContentType("application/json; charset=utf-8;");
String message = "未检测到登录信息,请登录!(在开发阶段,看到此提示时,请检查客户端是否携带了有效的JWT数据)";
log.warn(message);
JsonResult jsonResult = JsonResult.fail(ServiceCode.ERR_UNAUTHORIZED, message);
String jsonResultString = JSON.toJSONString(jsonResult);
PrintWriter writer = response.getWriter();
writer.println(jsonResultString);
writer.close();
}
});
客户端携带JWT提交请求
在客户端,当用户提交登录请求,且服务器端验证登录通过后,会向客户端响应“登录成功”的业务状态码及JWT数据,则客户端需要将此JWT数据保存下来,以便于后续在其它页面中可以取出,并用于提交后续的请求。
例如,在登录成功后:
使用axios时,需要调用create()来自定义请求头,此方法将返回一个新的axios对象,例如:
关于复杂请求的跨域访问
// 此方法会配置Spring Security框架自带的CorsFilter,此过滤器会对OPTIONS请求放行
http.cors();
提示:对于复杂请求的预检(提交同一个URL的OPTIONS请求)是客户端的浏览器的自主行为,并不是服务器端的要求,并且,对于同一个URL,如果预检通过,浏览器会缓存“预检通过”这个结果,并且,在后续的访问中,不再执行预检。