JWT中主要包括三个部分:
1、头部:包含签名的加密算法和token类型。将这个json串用base64url进行编码即形成了第一部分的token。
2、载荷:包括用户id、用户名、过期时间等,但是不包括用户的敏感信息,因为可以被反解出来。将这个json串用base64url进行编码即形成了第二部分的token。
3、签名:将前两部分的密文用头部指定的加密算法进行加盐加密(必须保证这个盐只有认证中心才知道),之后再用base64url编码。
为了保证盐的私密性,可采用RSA非对称加密方式。
RSA通俗理解:
如果是加密,那肯定是不希望别人知道我的消息,所以只有我才能解密,所以可得出公钥负责加密,私钥负责解密。
如果是签名,那肯定是不希望有人冒充我发消息,只有我才能发布这个签名,所以可得出私钥负责签名,公钥负责验证。
单点登录,简称SSO,说到底还是分布式认证,即我们常说的指的是在多应用系统的项目中,用户只需要登录一次,就可以访问所有互相信任的应用系统。
SSO实现起来比较简单,从分布式认证流程中,起到最关键作用的就是token,token的安全与否,直接关系到系统的健壮性,所以我们可以使用成熟的JWT来实现token的生成和校验。
其实上面的流程图还是比较复杂的,需要客户端一直保持和认证中心的全局会话,如果使用JWT的话,可以简化相应步骤为下图(因为JWT的载荷部分已经存储了过期时间,只要其他关联系统存储这相同的JWT解析规则就没必要一直和认证中心保持着全局会话了):
先使用RSA生成一套公钥和私钥,私钥只保存在认证中心处。之前用过SoringSecurity的可以直接它的的过滤器链,登陆的时候往认证中心发送用户名、密码,成功认证后,不仅给出用户的角色信息,还将JWT生成的token令牌放入响应头中。之后用户每次发请求都带上这个token,JWT解析规则由认证中心及关联的系统共享,其实这就已经完成了单点登录,还是很简单的。
编写认证中心的认证过滤器。
public class JwtLoginFilter extends UsernamePasswordAuthenticationFilter {
private AuthenticationManager authenticationManager;
private RsaKeyProperties prop;
public JwtLoginFilter(AuthenticationManager authenticationManager, RsaKeyProperties prop) {
this.authenticationManager = authenticationManager;
this.prop = prop;
}
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
try {
SysUser user = new ObjectMapper().readValue(request.getInputStream(), SysUser.class);
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(user.getUsername(), user.getPassword());
//继续调用自定义的UserDetailsService进行判断
return authenticationManager.authenticate(authRequest);
} catch (Exception e) {
try {
response.setContentType("application/json;charset=utf-8");
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
PrintWriter out = response.getWriter();
Map<String, Object> resultMap = new HashMap<>();
resultMap.put("code", HttpServletResponse.SC_UNAUTHORIZED);
resultMap.put("msg", "用户名或密码错误");
out.write(new ObjectMapper().writeValueAsString(resultMap));
out.flush();
out.close();
} catch (IOException ex) {
ex.printStackTrace();
}
}
return null;
}
@Override
public void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
SysUser user = new SysUser();
user.setUsername(authResult.getName());
user.setRoles((List<SysRole>) authResult.getAuthorities());
//密码不能放入token中
//私钥加密
String token = JwtUtils.generateToken(user, prop.getPrivateKey(), 1000 * 60 * 30);//半小时后过期
//生成token并放到响应的消息头中
response.addHeader("Authorization", "Bearer " + token);
try {
response.setContentType("application/json;charset=utf-8");
response.setStatus(HttpServletResponse.SC_OK);
PrintWriter out = response.getWriter();
Map<String, Object> resultMap = new HashMap<>();
resultMap.put("code", HttpServletResponse.SC_OK);
resultMap.put("msg", "登录成功");
out.write(new ObjectMapper().writeValueAsString(resultMap));
out.flush();
out.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
public class JwtVerifyFilter extends BasicAuthenticationFilter {
private RsaKeyProperties prop;
public JwtVerifyFilter(AuthenticationManager authenticationManager, RsaKeyProperties prop) {
super(authenticationManager);
this.prop = prop;
}
@Override
public void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
String header = request.getHeader("Authorization");
if (StringUtils.isEmpty(header) || !header.startsWith("Bearer ")) {
//携带错误格式的token
chain.doFilter(request, response);
responseJson(response);
return;
} else {
//token格式正确, 但还需要校验token的正确性
String token = header.replace("Bearer ", "");
try {
Payload<SysUser> payload = JwtUtils.parseToken(token, prop.getPublicKey(), SysUser.class);
SysUser user = JsonUtils.toBean(JsonUtils.toString(payload.getUserInfo()), SysUser.class);
UsernamePasswordAuthenticationToken authResult = null;
if (user != null) {
//因为在生成token的时候没有传password所以这里第二个参数为空, 第三个参数是认证成功后的角色信息
authResult = new UsernamePasswordAuthenticationToken(user.getUsername(), null, user.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(authResult);
chain.doFilter(request, response);
} else {
response.setContentType("application/json;charset=utf-8");
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
PrintWriter out = response.getWriter();
Map<String, Object> resultMap = new HashMap<>();
resultMap.put("code", HttpServletResponse.SC_UNAUTHORIZED);
resultMap.put("msg", "认证失败");
out.write(new ObjectMapper().writeValueAsString(resultMap));
out.flush();
out.close();
}
} catch (Exception ex) {
responseJson(response);
}
}
}
private void responseJson(HttpServletResponse response) throws IOException {
response.setContentType("application/json;charset=utf-8");
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
PrintWriter out = response.getWriter();
Map<String, Object> resultMap = new HashMap<>();
resultMap.put("code", HttpServletResponse.SC_UNAUTHORIZED);
resultMap.put("msg", "请登录");
out.write(new ObjectMapper().writeValueAsString(resultMap));
out.flush();
out.close();
}
}
OAuth是Open Authorization的简写。 OAuth协议为用户资源的授权提供了一个安全的、开放而又简易的标准,其实就是允许用户提供一个令牌,而不是用户名和密码来访问他们存放在特定服务提供者的数据。每一个令牌授权一个特定的网站。
A网站是一个打印照片的网站,B网站是一个存储照片的网站,二者原本毫无关联。
如果一个用户想使用A网站打印自己存储在B网站的照片,那么A网站就需要使用B网站的照片资源才行。 按照传统的思考模式,我们需要A网站具有登录B网站的用户名和密码才行,但是,现在有了OAuth2,只需要A网站获取到使用B网站照片资源的一个通行令牌即可!这个令牌无需具备操作B网站所有资源的权限,也无需永久有效,只要满足A网站打印照片需求即可。
这么说和单点登录有一点点像?其实还是不同的。
单点登录是用户一次登录,自己可以操作其他关联的服务资源。
OAuth2则是用户给一个系统授权,可以直接操作其他系统资源的一种方式。
直接上OAuth2的授权码模式流程图,图说的很形象了。
配合文字看图。
用户登录A系统进行照片打印,但是打印的照片呢存储在B系统上面,A系统需要提供一个重定向的URL,B系统作为OAuth2的资源服务。既然用户想间接访问系统B,那势必要有B系统的权限,A系统的客户端信息存储到OAuth2的认证服务中,进行认证之后B系统返回一个认证码并重定向到之前A系统提供的URL,A系统再通过这个认证码和自己在OAuth2认证服务上存储的客户端信息发送给OAuth2的认证服务端,服务端发放通行令牌给A系统。OK,此时A系统就能打印存储在B系统上的资源了。