众所周知,HTTP是无状态的协议,而如何识别请求呢,Spring Security为我们提供了Remember-Me功能。当然也可以用其他方式,诸如session、cookie等,这里推荐JWT。
一、Remember-Me功能概述
Remember-Me是指网站能够在Session之间记住登录用户的身份,具体来说就是我成功认证一次之后在一定的时间内我可以不用再输入用户名和密码进行登录了,系统会自动给我登录。这通常是通过服务端发送一个cookie给客户端浏览器,下次浏览器再访问服务端时服务端能够自动检测客户端的cookie,根据cookie值触发自动登录操作。Spring Security为这些操作的发生提供必要的钩子,并且针对于Remember-Me功能有两种实现。一种是简单的使用加密来保证基于cookie的token的安全,另一种是通过数据库或其它持久化存储机制来保存生成的token。
1.基于简单加密token的方法
当用户选择了记住我成功登录后,Spring Security将会生成一个cookie发送给客户端浏览器。cookie值由如下方式组成:
base64(username+":"+expirationTime+":"+md5Hex(username+":"+expirationTime+":"+password+":"+key))
Øusername:登录的用户名。
Øpassword:登录的密码。
ØexpirationTime:token失效的日期和时间,以毫秒表示。
Økey:用来防止修改token的一个key。
这样用来实现Remember-Me功能的token只能在指定的时间内有效。需要注意的是,这样做其实是存在安全隐患的,那就是在用户获取到实现记住我功能的token后,任何用户都可以在该token过期之前通过该token进行自动登录。如果用户发现自己的token被盗用了,那么他可以通过改变自己的登录密码来立即使其所有的记住我token失效。如果希望我们的应用能够更安全一点,可以使用接下来要介绍的持久化token方式,或者不使用Remember-Me功能,因为Remember-Me功能总是有点不安全的。
2.基于持久化token的方法
持久化token的方法跟简单加密token的方法在实现Remember-Me功能上大体相同,都是在用户选择了“记住我”成功登录后,将生成的token存入cookie中并发送到客户端浏览器,待到下次用户访问系统时,系统将直接从客户端cookie中读取token进行认证。所不同的是基于简单加密token的方法,一旦用户登录成功后,生成的token将在客户端保存一段时间,如果用户不点击退出登录,或者不修改密码,那么在cookie失效之前,他都可以使用该token进行登录,哪怕该token被别人盗用了,用户与盗用者都同样可以进行登录。而基于持久化token的方法采用这样的实现逻辑:
(1)用户选择了“记住我”成功登录后,将会把username、随机产生的序列号、生成的token存入一个数据库表中,同时将它们的组合生成一个cookie发送给客户端浏览器。
(2)当下一次没有登录的用户访问系统时,首先检查cookie,如果对应cookie中包含的username、序列号和token与数据库中保存的一致,则表示其通过验证,系统将重新生成一个新的token替换数据库中对应组合的旧token,序列号保持不变,同时删除旧的cookie,重新生成包含新生成的token,就的序列号和username的cookie发送给客户端。(3)如果检查cookie时,cookie中包含的username和序列号跟数据库中保存的匹配,但是token不匹配。这种情况极有可能是因为你的cookie被人盗用了,由于盗用者使用你原本通过认证的cookie进行登录了导致旧的token失效,而产生了新的token。这个时候Spring Security就可以发现cookie被盗用的情况,它将删除数据库中与当前用户相关的所有token记录,这样盗用者使用原有的cookie将不能再登录,同时提醒用户其帐号有被盗用的可能性。
(4)如果对应cookie不存在,或者包含的username和序列号与数据库中保存的不一致,那么将会引导用户到登录页面。
从以上逻辑我们可以看出持久化token的方法比简单加密token的方法更安全,因为一旦你的cookie被人盗用了,你只要再利用原有的cookie试图自动登录一次,原有的token将失效导致盗用者不能再使用原来盗用的cookie进行登录了,同时用户可以发现自己的cookie有被盗用的可能性。但因为cookie被盗用后盗用者还可以在用户下一次登录前顺利的进行登录,所以如果你的应用对安全性要求比较高就不要使用Remember-Me功能了。
二、Spring Security结合JWT实现
JWT自身包含校验所需的一些信息,就不需要在服务器存储session或者token了。下面就看看Spring Security如何整合JWT:
第一步:
当然是引入jwt了。
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.0</version>
</dependency>
第二步:
修改Spring Security过滤链中的登录认证过滤器:UsernamePasswordAuthenticationFilter,和认证过滤器:BasicAuthenticationFilter。认证通过后,服务器生成一个token,将该token返回给客户端,客户端以后的所有请求都需要在http头中指定该token。服务器接收的请求后,会对token的合法性进行验证。验证的内容包括:内容是一个正确的JWT格式、检查签名、检查claims、检查权限。
public class JWTLoginFilter extends UsernamePasswordAuthenticationFilter {
private AuthenticationManager authenticationManager;
public JWTLoginFilter(AuthenticationManager authenticationManager) {
this.authenticationManager = authenticationManager;
}
// 接收并解析用户凭证
@Override
public Authentication attemptAuthentication(HttpServletRequest req,
HttpServletResponse res) throws AuthenticationException {
logger.info("JWTLoginFilter:1登录成功!");
try {
byte info[]= new byte[1024];
req.getInputStream().read(info);
logger.info("==1"+new String(info));
//解析数据
// UserInfoBean user = new ObjectMapper()
// .readValue(req.getInputStream(), UserInfoBean.class);
UserInfoBean user = new UserInfoBean();
user.setUsername("111");
user.setPassword("111");
return authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(
user.getUsername(),
user.getPassword(),
new ArrayList<>())
);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
// 用户成功登录后,这个方法会被调用,我们在这个方法里生成token
@Override
protected void successfulAuthentication(HttpServletRequest req,
HttpServletResponse res,
FilterChain chain,
Authentication auth) throws IOException, ServletException {
logger.info("JWTLoginFilter:2登录成功!");
String token = Jwts.builder()
.setSubject(((org.springframework.security.core.userdetails.User) auth.getPrincipal()).getUsername())
.setExpiration(new Date(System.currentTimeMillis() + 60 * 60 * 24 * 1000))
.signWith(SignatureAlgorithm.HS512, "MyJwtSecret")
.compact();
res.addHeader("Authorization", "Bearer " + token);
}
}
/**
* token的校验
* 该类继承自BasicAuthenticationFilter,在doFilterInternal方法中,
* 从http头的Authorization 项读取token数据,然后用Jwts包提供的方法校验token的合法性。
* 如果校验通过,就认为这是一个取得授权的合法请求
* @author zhaoxinguo on 2017/9/13.
*/
public class JWTAuthenticationFilter extends BasicAuthenticationFilter {
public JWTAuthenticationFilter(AuthenticationManager authenticationManager) {
super(authenticationManager);
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
String header = request.getHeader("Authorization");
if (header == null || !header.startsWith("Bearer ")) {
chain.doFilter(request, response);
return;
}
UsernamePasswordAuthenticationToken authentication = getAuthentication(request);
SecurityContextHolder.getContext().setAuthentication(authentication);
chain.doFilter(request, response);
}
private UsernamePasswordAuthenticationToken getAuthentication(HttpServletRequest request) {
logger.info("JWTAuthenticationFilter:登录成功!");
String token = request.getHeader("Authorization");
if (token != null) {
// parse the token.
String user = Jwts.parser()
.setSigningKey("MyJwtSecret")
.parseClaimsJws(token.replace("Bearer ", ""))
.getBody()
.getSubject();
if (user != null) {
return new UsernamePasswordAuthenticationToken(user, null, new ArrayList<>());
}
return null;
}
return null;
}
}
第三步:
将这两个过滤器加入Spring Security过滤链
@Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin() //表单登录
//.loginPage("/evolutionary-loginIn.html")
.loginPage("/logintype") //如果需要身份认证则跳转到这里
.loginProcessingUrl("/login")
.successHandler(evolutionaryAuthenticationHandler)
.failureHandler(evolutionaryAuthenticationFailureHandler)
.and()
.authorizeRequests()
.antMatchers("/logintype",securityProperties.getBrower().getLoginPage())//不校验我们配置的登录页面
.permitAll()
.anyRequest()
.authenticated()
.and().csrf().disable().addFilter(new JWTLoginFilter(authenticationManager()))
.addFilter(new JWTAuthenticationFilter(authenticationManager())) ;
}
使用这种方式后,会导致之前配置的登录成功处理和失败处理的handler失效。