JWT是JSON Web Token的缩写,是目前最流行的跨域认证解决方法。
互联网服务认证的一般流程是:
上面的认证模式,存在以下缺点:
JWT认证原理是:
Authorization
里JWT token令牌可以包含用户身份、登录时间等信息,这样登录状态保持者由服务器端变为客户端,服务器变成无状态了;token放到请求头,实现了跨域
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
JWT由三部分组成:
表现形式为:Header.Payload.Signature
Header 部分是一个 JSON 对象,描述 JWT 的元数据,通常是下面的样子:
{
"alg": "HS256",
"typ": "JWT"
}
上面代码中,alg
属性表示签名的算法(algorithm),默认是 HMAC SHA256(写成 HS256);typ
属性表示这个令牌(token)的类型(type),JWT 令牌统一写为JWT
。
上面的 JSON 对象使用 Base64URL 算法转成字符串
Payload 部分也是一个 JSON 对象,用来存放实际需要传递的数据。JWT 规定了7个官方字段:
iss (issuer):签发人
exp (expiration time):过期时间
sub (subject):主题
aud (audience):受众
nbf (Not Before):生效时间
iat (Issued At):签发时间
jti (JWT ID):编号
当然,用户也可以定义私有字段。
这个 JSON 对象也要使用 Base64URL 算法转成字符串
Signature 部分是对前两部分的签名,防止数据篡改
签名算法如下:
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
your-256-bit-secret
)
算出签名以后,把 Header、Payload、Signature 三个部分拼成一个字符串,每个部分之间用"."分隔
Security是基于AOP和Servlet过滤器的安全框架,为了实现JWT要重写那些方法、自定义那些过滤器需要首先了解security自带的过滤器。security默认过滤器链如下:
这个过滤器有两个作用:
由于禁用session功能,所以该过滤器只剩一个作用即把SecurityContextHolder的securitycontext清空。举例来说明为何要清空securitycontext:用户1发送一个请求,由线程M处理,当响应完成线程M放回线程池;用户2发送一个请求,本次请求同样由线程M处理,由于securitycontext没有清空,理应储存用户2的信息但此时储存的是用户1的信息,造成用户信息不符
UsernamePasswordAuthenticationFilter
继承自AbstractAuthenticationProcessingFilter
,处理逻辑在doFilter
方法中:
UsernamePasswordAuthenticationFilter
拦截时,判断请求路径是否匹配登录URL,若不匹配继续执行下个过滤器;否则,执行步骤2attemptAuthentication
方法进行认证。UsernamePasswordAuthenticationFilter
重写了attemptAuthentication
方法,负责从读取登录参数,委托AuthenticationManager
进行认证,返回一个认证过的token(null表示认证失败)successfulAuthentication
。该方法把认证过的token放入securitycontext供后续请求授权,同时该方法预留一个扩展点(AuthenticationSuccessHandler.onAuthenticationSuccess方法
),进行认证成功后的处理uthenticationFailureHandler.onAuthenticationFailure
进行认证失败后的处理UsernamePasswordAuthenticationFilter
的attemptAuthentication
方法,执行逻辑如下:
HttpServletRequest.getParameter
方法获取参数,它只能处理Content-Type为application/x-www-form-urlencoded或multipart/form-data的请求,若是application/json则无法获取值UsernamePasswordAuthenticationToken
对象,创建未认证的token。UsernamePasswordAuthenticationToken
有两个重载的构造方法,其中public UsernamePasswordAuthenticationToken(Object principal, Object credentials)
创建未经认证的token,public UsernamePasswordAuthenticationToken(Object principal, Object credentials, Collection<? extends GrantedAuthority> authorities)
创建已认证的tokenAuthenticationManager
,其缺省实现为ProviderManager
,调用其authenticate
进行认证ProviderManager
的authenticate
是个模板方法,它遍历所有AuthenticationProvider
,直至找到支持认证某类型token的AuthenticationProvider
,调用AuthenticationProvider.authenticate
方法认证,AuthenticationProvider.authenticate
加载正确的账号、密码进行比较验证AuthenticationManager.authenticate
方法返回一个已认证的tokenAnonymousAuthenticationFilter
负责创建匿名token:
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
if (SecurityContextHolder.getContext().getAuthentication() == null) {
SecurityContextHolder.getContext().setAuthentication(this.createAuthentication((HttpServletRequest)req));
if (this.logger.isTraceEnabled()) {
this.logger.trace(LogMessage.of(() -> {
return "Set SecurityContextHolder to " + SecurityContextHolder.getContext().getAuthentication();
}));
} else {
this.logger.debug("Set SecurityContextHolder to anonymous SecurityContext");
}
} else if (this.logger.isTraceEnabled()) {
this.logger.trace(LogMessage.of(() -> {
return "Did not set SecurityContextHolder since already authenticated " + SecurityContextHolder.getContext().getAuthentication();
}));
}
chain.doFilter(req, res);
}
如果当前用户没有认证,会创建一个匿名token,用户是否能读取资源交由FilterSecurityInterceptor
过滤器委托给决策管理器判断是否有权限读取
JWT认证思路:
AuthenticationSuccessHandler
认证成功处理器,由该处理器生成token令牌JWT授权思路:
JWT的Java实现,利用开源的java-jwt
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.12.0</version>
</dependency>
我们对java-jwt提供的API进行封装,便于创建、验证、提取claim
@Slf4j
public class JWTUtil {
// 携带token的请求头名字
public final static String TOKEN_HEADER = "Authorization";
//token的前缀
public final static String TOKEN_PREFIX = "Bearer ";
// 默认密钥
public final static String DEFAULT_SECRET = "mySecret";
// 用户身份
private final static String ROLES_CLAIM = "roles";
// token有效期,单位分钟;
private final static long EXPIRE_TIME = 5 * 60 * 1000;
// 设置Remember-me功能后的token有效期
private final static long EXPIRE_TIME_REMEMBER = 7 * 24 * 60 * 60 * 1000;
// 创建token
public static String createToken(String username, List role, String secret, boolean rememberMe) {
Date expireDate = rememberMe ? new Date(System.currentTimeMillis() + EXPIRE_TIME_REMEMBER) : new Date(System.currentTimeMillis() + EXPIRE_TIME);
try {
// 创建签名的算法实例
Algorithm algorithm = Algorithm.HMAC256(secret);
String token = JWT.create()
.withExpiresAt(expireDate)
.withClaim("username", username)
.withClaim(ROLES_CLAIM, role)
.sign(algorithm);
return token;
} catch (JWTCreationException jwtCreationException) {
log.warn("Token create failed");
return null;
}
}
// 验证token
public static boolean verifyToken(String token, String secret) {
try{
Algorithm algorithm = Algorithm.HMAC256(secret);
// 构建JWT验证器,token合法同时pyload必须含有私有字段username且值一致
// token过期也会验证失败
JWTVerifier verifier = JWT.require(algorithm)
.build();
// 验证token
DecodedJWT decodedJWT = verifier.verify(token);
return true;
} catch (JWTVerificationException jwtVerificationException) {
log.warn("token验证失败");
return false;
}
}
// 获取username
public static String getUsername(String token) {
try {
// 因此获取载荷信息不需要密钥
DecodedJWT jwt = JWT.decode(token);
return jwt.getClaim("username").asString();
} catch (JWTDecodeException jwtDecodeException) {
log.warn("提取用户姓名时,token解码失败");
return null;
}
}
public static List<String> getRole(String token) {
try {
// 因此获取载荷信息不需要密钥
DecodedJWT jwt = JWT.decode(token);
// asList方法需要指定容器元素的类型
return jwt.getClaim(ROLES_CLAIM).asList(String.class);
} catch (JWTDecodeException jwtDecodeException) {
log.warn("提取身份时,token解码失败");
return null;
}
}
}
验证账号、密码交给UsernamePasswordAuthenticationFilter
,不用修改代码
认证成功后,需要生成token返回给客户端,我们通过扩展AuthenticationSuccessHandler.onAuthenticationSuccess方法
实现
@Component
public class JWTAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
@Override
public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
ResponseData responseData = new ResponseData();
responseData.setCode("200");
responseData.setMessage("登录成功!");
// 提取用户名,准备写入token
String username = authentication.getName();
// 提取角色,转为List<String>对象,写入token
List<String> roles = new ArrayList<>();
Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
for (GrantedAuthority authority : authorities){
roles.add(authority.getAuthority());
}
// 创建token
String token = JWTUtil.createToken(username, roles, JWTUtil.DEFAULT_SECRET, true);
httpServletResponse.setCharacterEncoding("utf-8");
// 为了跨域,把token放到响应头WWW-Authenticate里
httpServletResponse.setHeader("WWW-Authenticate", JWTUtil.TOKEN_PREFIX + token);
// 写入响应里
ObjectMapper mapper = new ObjectMapper();
mapper.writeValue(httpServletResponse.getWriter(), responseData);
}
}
为了统一返回值,我们封装了一个ResponseData
对象
自定义一个过滤器JWTAuthorizationFilter
,验证token,token验证成功后认为认证成功
@Slf4j
public class JWTAuthorizationFilter extends OncePerRequestFilter {
private UserDetailsService userDetailsService;
// 父类没有无参构造器,必须显示调用父类的含参构造器
public JWTAuthorizationFilter(UserDetailsService userDetailsService) {
super();
this.userDetailsService = userDetailsService;
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
String token = getTokenFromRequestHeader(request);
Authentication verifyResult = verefyToken(token, JWTUtil.DEFAULT_SECRET);
if (verifyResult == null) {
// 即便验证失败,也继续调用过滤链,匿名过滤器生成匿名令牌
chain.doFilter(request, response);
return;
} else {
log.info("token令牌验证成功");
SecurityContextHolder.getContext().setAuthentication(verifyResult);
chain.doFilter(request, response);
}
}
// 从请求头获取token
private String getTokenFromRequestHeader(HttpServletRequest request) {
String header = request.getHeader(JWTUtil.TOKEN_HEADER);
if (header == null || !header.startsWith(JWTUtil.TOKEN_PREFIX)) {
log.info("请求头不含JWT token, 调用下个过滤器");
return null;
}
String token = header.split(" ")[1].trim();
return token;
}
// 验证token,并生成认证后的token
private UsernamePasswordAuthenticationToken verefyToken(String token, String secret) {
if (token == null) {
return null;
}
// 认证失败,返回null
if (!JWTUtil.verifyToken(token, secret)) {
return null;
}
// 提取用户名
String username = JWTUtil.getUsername(token);
// 定义权限列表
List<GrantedAuthority> authorities = new ArrayList<>();
// 从token提取角色
List<String> roles = JWTUtil.getRole(token);
for (String role : roles) {
log.info("用户身份是:" + role);
authorities.add(new SimpleGrantedAuthority(role));
}
// 构建认证过的token
return new UsernamePasswordAuthenticationToken(username, null, authorities);
}
}
OncePerRequestFilter
保证当前请求中,此过滤器只被调用一次,执行逻辑在doFilterInternal
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
/*
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// 明文密码
//auth.inMemoryAuthentication().withUser("user").password("{noop}123456").roles();
auth.inMemoryAuthentication().withUser("admin").password(passwordEncoder().encode("123456")).roles();
}*/
@Autowired
private AjaxAuthenticationEntryPoint ajaxAuthenticationEntryPoint;
@Autowired
private AjaxAuthenticationSuccessHandler ajaxAuthenticationSuccessHandler;
@Autowired
private JWTAuthenticationSuccessHandler jwtAuthenticationSuccessHandler;
@Autowired
private AjaxAuthenticationFailureHandler ajaxAuthenticationFailureHandler;
@Autowired
private CustomUserDetailService customUserDetailService;
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.authorizeRequests().anyRequest().authenticated()
.and()
.formLogin()
.successHandler(jwtAuthenticationSuccessHandler)
.failureHandler(ajaxAuthenticationFailureHandler)
.permitAll()
.and()
.addFilterAfter(new JWTAuthorizationFilter(customUserDetailService), UsernamePasswordAuthenticationFilter.class)
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.exceptionHandling().authenticationEntryPoint(ajaxAuthenticationEntryPoint);
}
}
配置里取消了session功能,把我们定义的过滤器添加到过滤链中;同时,定义ajaxAuthenticationEntryPoint
处理未认证用户访问未授权资源时抛出的异常
@Component
public class AjaxAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
ResponseData responseData = new ResponseData();
responseData.setCode("401");
responseData.setMessage("匿名用户,请先登录再访问!");
httpServletResponse.setCharacterEncoding("utf-8");
ObjectMapper mapper = new ObjectMapper();
mapper.writeValue(httpServletResponse.getWriter(), responseData);
}
}
[Spring Security3源码分析(5)-SecurityContextPersistenceFilter分析](Spring Security3源码分析(5)-SecurityContextPersistenceFilter分析)
Spring Security addFilter() 顺序问题
前后端联调之Form Data与Request Payload,你真的了解吗?
Spring Boot 2 + Spring Security 5 + JWT 的单页应用 Restful 解决方案
原文:https://www.cnblogs.com/weixia-blog/p/14191109.html