使用Jwt生成token解决SSO单点登录以及OAuth2资源权限管理

一、JWT概述

       JWT中主要包括三个部分:

       1、头部:包含签名的加密算法和token类型。将这个json串用base64url进行编码即形成了第一部分的token。

       2、载荷:包括用户id、用户名、过期时间等,但是不包括用户的敏感信息,因为可以被反解出来。将这个json串用base64url进行编码即形成了第二部分的token。

       3、签名:将前两部分的密文用头部指定的加密算法进行加盐加密(必须保证这个盐只有认证中心才知道),之后再用base64url编码。

       为了保证盐的私密性,可采用RSA非对称加密方式


RSA通俗理解:
       如果是加密,那肯定是不希望别人知道我的消息,所以只有我才能解密,所以可得出公钥负责加密,私钥负责解密。
       如果是签名,那肯定是不希望有人冒充我发消息,只有我才能发布这个签名,所以可得出私钥负责签名,公钥负责验证。




二、SSO单点登录

1、SSO概述

       单点登录,简称SSO,说到底还是分布式认证,即我们常说的指的是在多应用系统的项目中,用户只需要登录一次,就可以访问所有互相信任的应用系统

使用Jwt生成token解决SSO单点登录以及OAuth2资源权限管理_第1张图片

       SSO实现起来比较简单,从分布式认证流程中,起到最关键作用的就是token,token的安全与否,直接关系到系统的健壮性,所以我们可以使用成熟的JWT来实现token的生成和校验。



2、流程梳理

       网上找的单点登录流程图:
使用Jwt生成token解决SSO单点登录以及OAuth2资源权限管理_第2张图片

       其实上面的流程图还是比较复杂的,需要客户端一直保持和认证中心的全局会话,如果使用JWT的话,可以简化相应步骤为下图(因为JWT的载荷部分已经存储了过期时间,只要其他关联系统存储这相同的JWT解析规则就没必要一直和认证中心保持着全局会话了):
使用Jwt生成token解决SSO单点登录以及OAuth2资源权限管理_第3张图片



3、JWT+SpringSecurity单点登录解决步骤

       先使用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();
	    }
	}





三、OAuth2

       OAuth是Open Authorization的简写。 OAuth协议为用户资源的授权提供了一个安全的、开放而又简易的标准,其实就是允许用户提供一个令牌,而不是用户名和密码来访问他们存放在特定服务提供者的数据。每一个令牌授权一个特定的网站


举个例子。

       A网站是一个打印照片的网站,B网站是一个存储照片的网站,二者原本毫无关联。
       如果一个用户想使用A网站打印自己存储在B网站的照片,那么A网站就需要使用B网站的照片资源才行。 按照传统的思考模式,我们需要A网站具有登录B网站的用户名和密码才行,但是,现在有了OAuth2,只需要A网站获取到使用B网站照片资源的一个通行令牌即可!这个令牌无需具备操作B网站所有资源的权限,也无需永久有效,只要满足A网站打印照片需求即可。

使用Jwt生成token解决SSO单点登录以及OAuth2资源权限管理_第4张图片



这么说和单点登录有一点点像?其实还是不同的。

单点登录是用户一次登录,自己可以操作其他关联的服务资源。
OAuth2则是用户给一个系统授权,可以直接操作其他系统资源的一种方式。



直接上OAuth2的授权码模式流程图,图说的很形象了。
使用Jwt生成token解决SSO单点登录以及OAuth2资源权限管理_第5张图片
       配合文字看图。
       用户登录A系统进行照片打印,但是打印的照片呢存储在B系统上面,A系统需要提供一个重定向的URL,B系统作为OAuth2的资源服务。既然用户想间接访问系统B,那势必要有B系统的权限,A系统的客户端信息存储到OAuth2的认证服务中,进行认证之后B系统返回一个认证码重定向到之前A系统提供的URLA系统再通过这个认证码和自己在OAuth2认证服务上存储的客户端信息发送给OAuth2的认证服务端,服务端发放通行令牌给A系统。OK,此时A系统就能打印存储在B系统上的资源了。

你可能感兴趣的:(java学习)