jwt

基本介绍

jwt 全称是jsonWebToken, 简单的说就是一种能够携带信息的token。
在传统的web环境中,浏览器和后端通过记录在浏览器的cookie 和存储在服务端的session 来实现登录状态,而cookie session的方式在多分布式环境下可能带来session复制,跨域访问,单点登录等问题;
直接使用后端生成token的方式,服务端也需要存储生成的token信息,因为token是无意义的。而使用jwt ,能够携带一些必要得信息比如用户id 和用户名称等;
后端就不需要对生成的token做存储,同时jwt也有时间的有效期。能够做到请求接口无状态;

缺点:

  1. 安全性,payload是使用base64编码的,并没有加密,因此jwt中不能存储敏感数据。而session的信息是存在服务端的,相对来说更安全。
  2. 无法废弃,只能等待过期失效,或增加其他的黑名单类似的逻辑处理失效。

jwt 官网: https://jwt.io/

格式

在使用过程中是一个base64编码的字符串

1
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

此字符串通过逗号分割是由3部分组成

第一部分是 header 区域,只要表示当前签名的加密方式;
第二部分是 plaoyload 区域,存储了当前的token携带的信息,包含颁发给谁,有效期等
第三部分是 将前2部分通过加密生成的,主要用于服务端校验token的合法性;

使用

基本依赖

引用对应的依赖,关于jwt的工具类有很多,这里使用 https://github.com/jwtk/jjwt

引入maven依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<!--api->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.11.2</version>
</dependency>
<!---实现-->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.11.2</version>
<scope>runtime</scope>
</dependency>


创建token使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31

//设置自定义header 信息
JwtBuilder jwtBuilder = Jwts.builder().setHeaderParam("a", "b");

Calendar instance = Calendar.getInstance();
instance.add(Calendar.MINUTE,30);

Date expire = instance.getTime();

//设置playload信息
jwtBuilder = jwtBuilder.setIssuer("me") //谁颁发的
.setSubject("Bob") // token的主体是什么 ,是关于什么的
.setAudience("you") // 给谁的
.setExpiration(expire) //失效时间
.setNotBefore(new Date()) //不能在此时间之前获取
.setIssuedAt(new Date()) //签发时间
.setId(UUID.randomUUID().toString());//id

//设置自定义的playload信息
jwtBuilder.claim("key","value");

//构建签名算法,更多签名算法查看 SignatureAlgorithm
Key key = Keys.secretKeyFor(SignatureAlgorithm.HS256);
jwtBuilder = jwtBuilder.signWith(key);

//执行压缩 使生成的字符串变小
jwtBuilder = jwtBuilder.compressWith(CompressionCodecs.DEFLATE);

System.out.println(jwtBuilder.compact());


解析读取token

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

JwtParserBuilder jwtParserBuilder = Jwts.parserBuilder();
//设置解析的签名算法
jwtParserBuilder = jwtParserBuilder.setSigningKey(key);

Jws<Claims> claimsJws = jwtParserBuilder.build().parseClaimsJws(jwtStr);

String signature = claimsJws.getSignature();
System.out.println("<========>");
System.out.println(signature);
JwsHeader header = claimsJws.getHeader();
System.out.println(header);
Claims body = claimsJws.getBody();
System.out.println(body);


jackjson 的支持

引入依赖

1
2
3
4
5
6
7
8
9
10


<!--jackson 支持-->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId> <!-- or jjwt-gson if Gson is preferred -->
<version>0.11.2</version>
<scope>runtime</scope>


1
2
3
4
5
6
//设置序列化方式
jwtBuilder = jwtBuilder.serializeToJsonWith(new JacksonSerializer());

//设置反序列化方式
jwtParserBuilder = jwtParserBuilder.deserializeJsonWith(new JacksonDeserializer());

springsecurity 整合jwt

首先回忆一下springsecurity 的默认登录和鉴权流程;

springsecurity 中主要由一整套过滤器链来处理,不同的过滤器处理不同的功能;

  • ChannelProcessingFilter,因为它可能需要重定向到不同的协议
  • SecurityContextPersistenceFilter,因此可以在web请求开头的SecurityContextHolder中设置SecurityContext,并且SecurityContext的任何更改都可以复制到HttpSession当web请求结束时(准备好与下一个web请求一起使用)
  • ConcurrentSessionFilter,因为它使用SecurityContextHolder功能并需要更新SessionRegistry以反映来自校长的持续请求
  • 身份验证处理机制 - UsernamePasswordAuthenticationFilterCasAuthenticationFilterBasicAuthenticationFilter等 - 以便SecurityContextHolder可以修改为包含有效的Authentication请求令牌
  • SecurityContextHolderAwareRequestFilter,如果您使用它将Spring Security识别HttpServletRequestWrapper安装到您的servlet容器中
  • JaasApiIntegrationFilter,如果SecurityContextHolder位于SecurityContextHolder,则会将FilterChain视为JaasAuthenticationToken中的Subject
  • RememberMeAuthenticationFilter,如果没有早期的身份验证处理机制更新SecurityContextHolder,并且请求提供了一个启用记住我服务的cookie,则会在那里放置一个合适的记忆Authentication对象
  • AnonymousAuthenticationFilter,如果没有早期的身份验证处理机制更新SecurityContextHolder,那么匿名Authentication对象将被放置在那里
  • ExceptionTranslationFilter,捕获任何Spring Security异常,以便可以返回HTTP错误响应或启动适当的AuthenticationEntryPoint
  • FilterSecurityInterceptor,用于保护web URI并在访问被拒绝时引发异常

对于一个具有session登录的流程的过滤器链执行顺序是;

当发起登录请求的时候:

UsernamePasswordAuthenticationFilter 处理登录请求的参数和处理 将登录的信息放入session中;同时将Authentication 放入SecurityContext 中;

再次发起请求时 由 SecurityContextPersistenceFilter 从请求中获取请求的session信息获取Authentication 用于后续的流程校验;

整合基本要做的有

  1. 禁用session,不需要做任何session相关的处理。
  2. 未登录的时候能返回一个json提示,而不是登录页面。
  3. 登录成功后以json的返回成功和失败信息和jwt字符串
  4. 后续请求在header中卸载jwt 需要通过一个自定义的filter 从header中解析出来任何设置到 SecurityContext 中;

jwt 工具类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.11.2</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.11.2</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.11.2</version>
<scope>compile</scope>
</dependency>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
//提供了2个方法都是从UserDetails 中生成信息和获取信息
public class JwtUtil {

private static Key key = Keys.secretKeyFor(SignatureAlgorithm.HS256);

private static String ISS_USER = "web";

private static String SUBJECT = "auth";



/**
* 创建jwt
* @param userDetails
* @return
*/
public static String createJwt(UserDetails userDetails){
JwtBuilder jwtBuilder = Jwts.builder();

Calendar instance = Calendar.getInstance();
instance.add(Calendar.MINUTE,30);
Date expire = instance.getTime();

jwtBuilder = jwtBuilder.setIssuer(ISS_USER)
.setSubject(SUBJECT)
.setAudience(userDetails.getUsername())
.setExpiration(expire)
.setNotBefore(new Date())
.setIssuedAt(new Date())
.setId(userDetails.getUsername());

JSONObject jsonObject = new JSONObject();
jsonObject.putOpt("roleCodes",((MyUserDetails)userDetails).getRoleCodes());
jsonObject.putOpt("permissionCodes",((MyUserDetails)userDetails).getPermissionCodes());
jwtBuilder.addClaims(jsonObject);

jwtBuilder = jwtBuilder.signWith(key);
//执行压缩 使生成的字符串变小
jwtBuilder = jwtBuilder.compressWith(CompressionCodecs.DEFLATE);
jwtBuilder = jwtBuilder.serializeToJsonWith(new JacksonSerializer());
return jwtBuilder.compact();
}


/**
* 解析jwt
* @param jwtStr
* @return
*/
public static UserDetails parseJwt(String jwtStr){
JwtParserBuilder jwtParserBuilder = Jwts.parserBuilder();
jwtParserBuilder = jwtParserBuilder.setSigningKey(key);
jwtParserBuilder = jwtParserBuilder.deserializeJsonWith(new JacksonDeserializer());
Jws<Claims> claimsJws = jwtParserBuilder.build().parseClaimsJws(jwtStr);

MyUserDetails userDetails = new MyUserDetails();
Claims claims = claimsJws.getBody();
userDetails.setUserName(claims.getId());
userDetails.setRoleCodes((List)claims.get("roleCodes"));
userDetails.setPermissionCodes((List)claims.get("permissionCodes"));
return userDetails;
}

}

配置无权限的json返回

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
@Bean
public AuthenticationEntryPoint authenticationEntryPoint(){
return new AuthenticationEntryPoint() {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
JSONObject jsonObject = new JSONObject();
jsonObject.putOpt("code","4001");
jsonObject.putOpt("message","未登录");
writeJson(response,jsonObject);
}
};
}

private void writeJson(HttpServletResponse response, JSONObject jsonObject){
try {
response.setCharacterEncoding("utf-8");
response.setContentType("application/json; charset=utf-8");
PrintWriter writer = response.getWriter();
writer.write(jsonObject.toString());
} catch (IOException e) {
e.printStackTrace();
}
}




@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().anyRequest().authenticated().and().
//配置exceptionHandling
exceptionHandling().authenticationEntryPoint(authenticationEntryPoint())

处理登录成功或失败的处理

在failureHandler 中处理登录错误的信息;

successHandler 中返回登录成功和jwt字符串信息;

sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) 禁用session

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().anyRequest().authenticated().and().
exceptionHandling().authenticationEntryPoint(authenticationEntryPoint())
.and()
.formLogin().loginProcessingUrl("/loginDo").failureHandler(new AuthenticationFailureHandler(){
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
JSONObject jsonObject = new JSONObject();
jsonObject.putOpt("code","4002");
jsonObject.putOpt("message","登录错误");
writeJson(response,jsonObject);
}
}).successHandler(new AuthenticationSuccessHandler() {
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
JSONObject jsonObject = new JSONObject();
jsonObject.putOpt("code","2000");
jsonObject.putOpt("message","登录成功");
//生成token
Object getPrincipalObj = authentication.getPrincipal();
if(getPrincipalObj instanceof MyUserDetails){
MyUserDetails details = (MyUserDetails)getPrincipalObj;
String jwt = JwtUtil.createJwt(details);
jsonObject.putOpt("token",jwt);
}
writeJson(response,jsonObject);
}
}).permitAll().and()
.csrf().disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and().addFilterBefore(new TokenAuthFilter(authenticationManager(),authenticationEntryPoint()),UsernamePasswordAuthenticationFilter.class).httpBasic();
}


添加 自定义解析jwtFilter

配置添加自定义的filter ,添加到 UsernamePasswordAuthenticationFilter 之前;

此filter主要逻辑是从header中解析 jwt 信息,如果能获取到就封装成UsernamePasswordAuthenticationToken 并设置到SecurityContext 中去.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
public class TokenAuthFilter extends BasicAuthenticationFilter  {


private AuthenticationEntryPoint authenticationEntryPoint;


public TokenAuthFilter(AuthenticationManager authenticationManager) {
super(authenticationManager);
}

public TokenAuthFilter(AuthenticationManager authenticationManager, AuthenticationEntryPoint authenticationEntryPoint) {
super(authenticationManager, authenticationEntryPoint);
}


@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
try {
UsernamePasswordAuthenticationToken authRequest = getUsernamePasswordAuthenticationToken(request);
if (authRequest == null) {
this.logger.trace("Did not process authentication request since failed to find "
+ "username and password in Basic Authorization header");
chain.doFilter(request, response);
return;
}
String username = authRequest.getName();
this.logger.trace(LogMessage.format("Found username '%s' in Basic Authorization header", username));
if (authenticationIsRequired(username)) {
SecurityContextHolder.getContext().setAuthentication(authRequest);
}
} catch (AuthenticationException ex) {
SecurityContextHolder.clearContext();
this.logger.debug("Failed to process authentication request", ex);
onUnsuccessfulAuthentication(request, response, ex);
this.authenticationEntryPoint.commence(request, response, ex);
return;
}
chain.doFilter(request, response);
}



private boolean authenticationIsRequired(String username) {

Authentication existingAuth = SecurityContextHolder.getContext().getAuthentication();
if (existingAuth == null || !existingAuth.isAuthenticated()) {
return true;
}

if (existingAuth instanceof UsernamePasswordAuthenticationToken && !existingAuth.getName().equals(username)) {
return true;
}

return (existingAuth instanceof AnonymousAuthenticationToken);
}




private UsernamePasswordAuthenticationToken getUsernamePasswordAuthenticationToken(HttpServletRequest request) {
String header = request.getHeader(HttpHeaders.AUTHORIZATION);
if (header == null) {
return null;
}
header = header.trim();
try{
UserDetails userDetails = JwtUtil.parseJwt(header);
UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(userDetails.getUsername(),
userDetails.getPassword(),userDetails.getAuthorities());
return token;
}catch (Exception e){
e.printStackTrace();
}
return null;
}

测试

未携带jwt 或jwt错误返回

1
2
3
4
{
"code": "4001",
"message": "未登录"
}

执行登录返回jwt信息;