Spring Security + JWT

Spring Security默认是基于session进行用户认证的,用户通过登录请求完成认证之后,认证信息在服务器端保存在session中,之后的请求发送上来后SecurityContextPersistenceFilter过滤器从session中获取认证信息、以便通过后续安全过滤器的安全检查。

今天的目标是替换Spring Security默认的session保存认证信息的机制为***通过JWT的方式进行认证。***

JWT(JSON WEB TOKEN)的相关内容就不做详细分析了,我们只需要知道以下几点:

1. 用户登录认证(用户名、密码验证)通过之后,系统生成token并送给前端。

2. token中包含用户id(或用户名)以及过期时间,包含通过加密机制生成的摘要,具有防篡改的能力。

3. token信息不需要在服务器端保存,前端获取到token之后,每次请求都必须携带该token。

4. 后台接收到请求之后,检查没有token、或者token验证不通过则不生成认证信息,否则,token验证通过则表示该用户通过认证。

5. 后台接收到的token如果已过期,则根据应用的需求自动更新token或者要求前端重新登录。

与session方案对比一下,我们需要解决的问题如下:

1. 需要停用掉Spring Security默认的session管理用户认证信息的方案。

2. 用户登录后需要生成并返回给前端token。

3. 前端请求上来之后,需要获取并验证token,验证通过后生成用户认证信息。

下面我们逐一解决上述三个问题。我们仍然使用上一篇文章中用过的demo,已经贴出过的代码就不再贴出了。

#### 准备工作

我们需要准备一些与JWT相关的东西,比如引入JWT的生成token、token验证的模块。

我们引入java-jwt,在pom文件加入依赖即可:

```

com.auth0

java-jwt

4.2.1

```

然后需要编写一个工具类,以便能够生成、验证token,我们暂时不考虑token过期等等细节问题的处理,只要能正确生成、验证token就可以:

```

public class JwtUtil {

public final static String SECRET_KEY="This is secret key for JWT";

public final static String JWTHeader_Leading_Str="Bearer ";

public final static String JWTHeader_Name="Authorization";

public static String generateToken(String userName){

Calendar calendar = Calendar.getInstance();

calendar.add(Calendar.SECOND,120);

HashMap header = new HashMap<>();

header.put("alg","HS256");

header.put("Type","JWT");

return JWT.create().withHeader(header)

.withClaim("userName",userName)

.withExpiresAt(calendar.getTime())

.sign(Algorithm.HMAC256(SECRET_KEY));

}

public static String verify(String token){

// 创建解析对象,使用的算法和secret要与创建token时保持一致

JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256(SECRET_KEY)).build();

// 解析指定的token

DecodedJWT decodedJWT = jwtVerifier.verify(token);

return decodedJWT.getClaims().get("userName").asString();

}

public static String parseToken(HttpServletRequest request){

String rawJwt = request.getHeader(JWTHeader_Name);

if(rawJwt==null){

return null;

}

if(!rawJwt.startsWith(JWTHeader_Leading_Str)){

return null;

}

return rawJwt.substring(JWTHeader_Leading_Str.length()+1);

}

private void showToken(DecodedJWT decodedJWT){

// 获取解析后的token中的信息

String header = decodedJWT.getHeader();

System.out.println("type:" + decodedJWT.getType());

System.out.println("header:" + header);

Map payloadMap = decodedJWT.getClaims();

System.out.println("Payload:" + payloadMap);

Date expires = decodedJWT.getExpiresAt();

System.out.println("过期时间:" + expires);

String signature = decodedJWT.getSignature();

System.out.println("signature:" + signature);

}

public static void main(String[] args) {

String token=JwtUtil.generateToken("Zhang Fu");

System.out.println(token);

String userName = JwtUtil.verify(token);

System.out.println("userName:" +userName);

}

}

```

OK,准备工作完成。

#### 停用Spring Security的默认session方案

为了停用session,我们需要增加一项配置,所以我们要新建一个配置文件:

```

@Configuration

public class WebSecurityConfig {

@Autowired

MyRememberMeService myRememberMeService;

@Bean

public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception

{

httpSecurity.authorizeRequests()

//.antMatchers("/hello").permitAll()

.anyRequest().authenticated().and()

.httpBasic().and()

.rememberMe().rememberMeServices(myRememberMeService)

.and().sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)

.and()

.formLogin();

//httpSecurity.addFilterBefore(new JwtSecurityFilter(), UsernamePasswordAuthenticationFilter.class);

//httpSecurity.addFilterAfter(new JwtAfterUsernamePasswordFilter(),UsernamePasswordAuthenticationFilter.class);

return httpSecurity.build();

}

}

```

***设置SessionCreationPolicy.STATELESS就可以达到目的。***

原因可以在sessionManagementConfigure.java这个session配置器中找到,在他的init方法中:

```

@Override

public void init(H http) {

SecurityContextRepository securityContextRepository = http.getSharedObject(SecurityContextRepository.class);

boolean stateless = isStateless();

if (securityContextRepository == null) {

if (stateless) {

http.setSharedObject(SecurityContextRepository.class, new NullSecurityContextRepository());

}

```

如果SessionCreationPolicy设置为stateless的话,那么他会创建NullSecurityContextRepository作为他的SecurityContextRepository。

这个NullSecurityContextRepository实际就是个假把式,啥也不干,我们知道用户认证通过后会调用他的saveContext方法存储认证信息,他是这么干的:

```

@Override

public void saveContext(SecurityContext context, HttpServletRequest request, HttpServletResponse response) {

}

```

所以,他就是个偷工减料的货,啥也没干。

所以第一个问题解决了。

#### 用户登录后生成token并返回给前端

这个问题我尝试了好几个方案之后才成功。

我们知道用户登录是在安全过滤器UsernamePasswordAuthenticationFilter中完成的,登录成功后如果想要生成JWT的token,方案无非就是:

1. UsernamePasswordAuthenticationFilter之后加一个我们自己的过滤器,与UsernamePasswordAuthenticationFilter一样只匹配登录请求,生成token。

2. UsernamePasswordAuthenticationFilter过滤器认证通过后有没有调用过其他可以被我们客户化的东东,我们客户化这个东东完成我们的目标。

3. 客户化UsernamePasswordAuthenticationFilter,登录成功后生成token。

这里必须交代一下,第3个方案只是从逻辑上来说应该能解决我们的问题,但是压根就没有考虑过这个方案,因为我觉得太麻烦。

先试了第一个方案,没成功,因为我们知道Spring Security还有一个RequestCacheAwareFilter过滤器,会导致如果你是在尚未获取授权之前访问了非登录页面,那么Spring Security会导航到登录页面、登录成功后在UsernamePasswordAuthenticationFilter中就会发生跳转,这样的话就跳过了我们后面加的这个过滤器,目标就无法实现或者说即使弯弯绕绕能实现,但是方案也不会太好。

所以,就努力研究第2个方案。

所以大概看了一下UsernamePasswordAuthenticationFilter在登录认证成功后的处理,发现了这个:

![image.png](/img/bVc472f)

所以就大概去研究了一下RememberMeServices,读了一下他的doc,发现他是一个基于cokie的、确保前台请求即使在session过期之后发送上来都可以继续通过安全认证的“记住我”机制。

除了cokie之外,其他的与JWT的要求完全吻合。如果我们能自己实现一个基于JWT的RememberMeService,是不是就解决问题了?

所以就用他来尝试一下。

#### MyRememberMeService#loginSuccess

创建MyRememberMeService并通过配置加入到应用中来,前面停用session的时候的配置文件中已经加入了,返回去看一眼就行。

我们要实现他的loginSuccess方法,创建并返回token:

```

@Override

public void loginSuccess(HttpServletRequest request, HttpServletResponse response,

Authentication successfulAuthentication) {

String username = successfulAuthentication.getName();

String token= JwtUtil.generateToken(username);

token=JwtUtil.JWTHeader_Leading_Str+token;

log.info("After login success:"+token);

response.setHeader(JwtUtil.JWTHeader_Name,token);

}

```

#### 验证一下创建并返回token

启动项目,成功登录系统后,惊喜的发现他已经开始干活了:

![image.png](/img/bVc479a)

好了,给了我们信心,撸起袖子加油干!

#### RememberMeAuthenticationFilter

RememberMeServices机制依赖RememberMeAuthenticationFilter实现,我们在上面的配置文件中已经启用了。

![image.png](/img/bVc472O)

然后简单看一眼RememberMeAuthenticationFilter过滤器的doFilter方法,他首先去SecurityContextHolder获取认证信息,如果没有获取到的话,就调用RememberMeService的autoLogin方法,只是从doFilter的源码来看(代码就不贴出了),autoLogin方法返回的Authentication并未完成认证,因为返回之后还要调用authenticationManager进行认证。

![image.png](/img/bVc478Q)

这是与我们预期不符的地方,我们希望autoLogin之后就可以完成认证、并且可以将认证信息放置到SecurityContextHolder中(因为我们是通过JWT做验证的,token验证通过的话就相当于完成了认证)。

***那我们是不是可以在autoLogin中完成这些操作,并且返回null骗一下RememberMeAuthenticationFilter的doFilter方法不再要求authenticationManager去再次认证呢?***

我们试一下!

#### MyRememberMeService#autoLogin

创建RememberMeService并实现autoLogin方法,为了简化他的初始化过程,我们直接把他注入到Spring Ioc容器中。

***如前所述,方法一定要返回null。***

```

@Slf4j

@Component

public class MyRememberMeService implements RememberMeServices {

@Autowired

MyUserDetailsService myUserDetailsService;

@Override

public Authentication autoLogin(HttpServletRequest request, HttpServletResponse response) {

log.info("autoLogin in MyRememberMeService: ");

String username;

String token = JwtUtil.parseToken(request);

if(token==null){

log.info("I dont get token from header");

token=request.getParameter("token");

}

log.info("finally the token is :" + token);

UsernamePasswordAuthenticationToken authenticationToken=null;

if(token!=null) {

String userName=JwtUtil.verify(token);

UserDetails user = myUserDetailsService.loadUserByUsername(userName);

authenticationToken = new UsernamePasswordAuthenticationToken(user, null, user.getAuthorities());

authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));

SecurityContextHolder.getContext().setAuthentication(authenticationToken);

}

return null;

}

@Override

public void loginFail(HttpServletRequest request, HttpServletResponse response) {

}

}

```

#### 测试一下autoLogin

上面的代码中已经看到了,我们只是为了测试、如果从请求头信息中拿不到token的话就从请求参数中获取。只是为了学习、测试偷个懒,正式项目实现的时候这个地方还是需要比较多的完善的。

启动项目,开始测试,第一步先通过login获取token,上面已经展示过了,然后用获取到的token发一个需要认证的请求,token加在请求参数后面:

![image.png](/img/bVc479l)

如图,请求成功了!

上一篇 [Spring Security自定义用户认证过程(2)](https://segmentfault.com/a/1190000043101057)

下一篇 [Mybatis拦截器](https://segmentfault.com/a/1190000043186656)

你可能感兴趣的:(java,spring,spring,boot)