其他分享
首页 > 其他分享> > 使用springboot,Oauth2.0,jwt令牌实现单点登录,权限控制等功能的基本流程

使用springboot,Oauth2.0,jwt令牌实现单点登录,权限控制等功能的基本流程

作者:互联网

本次主要是用来记录,使用springboot,Oauth2.0,jwt令牌的一些基本知识,以及利用以上几乎来实现单点登录,权限控制等功能的基本流程。

1.授权流程

本次授权流程如下图所示,采用私钥签名,公钥校验的方式来校验token令牌。
授权流程

2.公钥私钥

Spring Security 提供对JWT的支持,本节我们使用Spring Security 提供的JwtHelper来创建JWT令牌,校验JWT令牌 等操作。 这里JWT令牌我们采用非对称算法进行加密,所以我们要先生成公钥和私钥。

(1)生成密钥证书 下边命令生成密钥证书,采用RSA 算法每个证书包含公钥和私钥

创建一个文件夹,在该文件夹下执行如下命令行:

keytool -genkeypair -alias changgou -keyalg RSA -keypass changgou -keystore changgou.jks -storepass changgou 

Keytool 是一个java提供的证书管理工具

-alias:密钥的别名 
-keyalg:使用的hash算法 
-keypass:密钥的访问密码 
-keystore:密钥库文件名,xc.keystore保存了生成的证书 
-storepass:密钥库的访问密码 

(2)查询证书信息

keytool -list -keystore changgou.jks

(3)删除别名

keytool -delete -alias changgou -keystore changgou.jsk

4.2.3 导出公钥

openssl是一个加解密工具包,这里使用openssl来导出公钥信息。

安装 openssl:http://slproweb.com/products/Win32OpenSSL.html

安装资料目录下的Win64OpenSSL-1_1_0g.exe

配置openssl的path环境变量

cmd进入changgou.jks文件所在目录执行如下命令(如下命令在windows下执行,会把-变成中文方式,请将它改成英文的-):

keytool -list -rfc --keystore changgou.jks | openssl x509 -inform pem -pubkey

下面段内容是公钥

-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvFsEiaLvij9C1Mz+oyAm
t47whAaRkRu/8kePM+X8760UGU0RMwGti6Z9y3LQ0RvK6I0brXmbGB/RsN38PVnh
cP8ZfxGUH26kX0RK+tlrxcrG+HkPYOH4XPAL8Q1lu1n9x3tLcIPxq8ZZtuIyKYEm
oLKyMsvTviG5flTpDprT25unWgE4md1kthRWXOnfWHATVY7Y/r4obiOL1mS5bEa/
iNKotQNnvIAKtjBM4RlIDWMa6dmz+lHtLtqDD2LF1qwoiSIHI75LQZ/CNYaHCfZS
xtOydpNKq8eb1/PGiLNolD4La2zf0/1dlcr5mkesV570NxRmU1tFm8Zd3MZlZmyv
9QIDAQAB
-----END PUBLIC KEY-----

将上边的公钥拷贝到文本public.key文件中,合并为一行,可以将它放到需要实现授权认证的工程中。

3.创建jwt令牌

(1)创建令牌数据

在changgou-user-oauth工程中创建测试类com.changgou.token.CreateJwtTest,使用它来创建令牌信息,代码如下:

    public class CreateJwtTest {
    
        /***
         * 创建令牌测试
         */
        @Test
        public void testCreateToken(){
            //证书文件路径
            String key_location="changgou.jks";
            //秘钥库密码
            String key_password="changgou";
            //秘钥密码
            String keypwd = "changgou";
            //秘钥别名
            String alias = "changgou";
    
            //访问证书路径
            ClassPathResource resource = new ClassPathResource(key_location);
    
            //创建秘钥工厂
            KeyStoreKeyFactory keyStoreKeyFactory = new KeyStoreKeyFactory(resource,key_password.toCharArray());
    
            //读取秘钥对(公钥、私钥)
            KeyPair keyPair = keyStoreKeyFactory.getKeyPair(alias,keypwd.toCharArray());
    
            //获取私钥
            RSAPrivateKey rsaPrivate = (RSAPrivateKey) keyPair.getPrivate();
    
            //定义Payload
            Map<String, Object> tokenMap = new HashMap<>();
            tokenMap.put("id", "1");
            tokenMap.put("name", "itheima");
            tokenMap.put("roles", "ROLE_VIP,ROLE_USER");
    
            //生成Jwt令牌
            Jwt jwt = JwtHelper.encode(JSON.toJSONString(tokenMap), new RsaSigner(rsaPrivate));
    
            //取出令牌
            String encoded = jwt.getEncoded();
            System.out.println(encoded);
        }
    }

运行后的结果如下:

eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlcyI6IlJPTEVfVklQLFJPTEVfVVNFUiIsIm5hbWUiOiJpdGhlaW1hIiwiaWQiOiIxIn0.IR9Qu9ZqYZ2gU2qgAziyT38UhEeL4Oi69ko-dzC_P9-Vjz40hwZDqxl8wZ-W2WAw1eWGIHV1EYDjg0-eilogJZ5UikyWw1bewXCpvlM-ZRtYQQqHFTlfDiVcFetyTayaskwa-x_BVS4pTWAskiaIKbKR4KcME2E5o1rEek-3YPkqAiZ6WP1UOmpaCJDaaFSdninqG0gzSCuGvLuG40x0Ngpfk7mPOecsIi5cbJElpdYUsCr9oXc53ROyfvYpHjzV7c2D5eIZu3leUPXRvvVAPJFEcSBiisxUSEeiGpmuQhaFZd1g-yJ1WQrixFvehMeLX2XU6W1nlL5ARTpQf_Jjiw

(2)解析令牌

上面创建令牌后,我们可以对JWT令牌进行解析,这里解析需要用到公钥,我们可以将之前生成的公钥public.key拷贝出来用字符串变量token存储,然后通过公钥解密。

在changgou-user-oauth创建测试类com.changgou.token.ParseJwtTest实现解析校验令牌数据,代码如下:


    public class ParseJwtTest {
    
        /***
         * 校验令牌
         */
        @Test
        public void testParseToken(){
            //令牌
            String token = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlcyI6IlJPTEVfVklQLFJPTEVfVVNFUiIsIm5hbWUiOiJpdGhlaW1hIiwiaWQiOiIxIn0.IR9Qu9ZqYZ2gU2qgAziyT38UhEeL4Oi69ko-dzC_P9-Vjz40hwZDqxl8wZ-W2WAw1eWGIHV1EYDjg0-eilogJZ5UikyWw1bewXCpvlM-ZRtYQQqHFTlfDiVcFetyTayaskwa-x_BVS4pTWAskiaIKbKR4KcME2E5o1rEek-3YPkqAiZ6WP1UOmpaCJDaaFSdninqG0gzSCuGvLuG40x0Ngpfk7mPOecsIi5cbJElpdYUsCr9oXc53ROyfvYpHjzV7c2D5eIZu3leUPXRvvVAPJFEcSBiisxUSEeiGpmuQhaFZd1g-yJ1WQrixFvehMeLX2XU6W1nlL5ARTpQf_Jjiw";
    
            //公钥
            String publickey = "-----BEGIN PUBLIC KEY-----MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvFsEiaLvij9C1Mz+oyAmt47whAaRkRu/8kePM+X8760UGU0RMwGti6Z9y3LQ0RvK6I0brXmbGB/RsN38PVnhcP8ZfxGUH26kX0RK+tlrxcrG+HkPYOH4XPAL8Q1lu1n9x3tLcIPxq8ZZtuIyKYEmoLKyMsvTviG5flTpDprT25unWgE4md1kthRWXOnfWHATVY7Y/r4obiOL1mS5bEa/iNKotQNnvIAKtjBM4RlIDWMa6dmz+lHtLtqDD2LF1qwoiSIHI75LQZ/CNYaHCfZSxtOydpNKq8eb1/PGiLNolD4La2zf0/1dlcr5mkesV570NxRmU1tFm8Zd3MZlZmyv9QIDAQAB-----END PUBLIC KEY-----";
    
            //校验Jwt
            Jwt jwt = JwtHelper.decodeAndVerify(token, new RsaVerifier(publickey));
    
            //获取Jwt原始内容
            String claims = jwt.getClaims();
            System.out.println(claims);
            //jwt令牌
            String encoded = jwt.getEncoded();
            System.out.println(encoded);
        }
    }

运行后的结果如下:
![

在这里插入图片描述

](https://www.icode9.com/i/ll/?i=20200529054527527.png)

4.Oauth微服务开发

  1. controller层提供了一个认证接口/user/login
@RestController
@RequestMapping("/user")
public class AuthController {

    @Autowired
    AuthService authService;

    @Value("${auth.clientId}")
    private String clientId;

    @Value("${auth.clientSecret}")
    private String clientSecret;

    @Value("${auth.cookieDomain}")
    private String cookieDomain;

    @Value("${auth.cookieMaxAge}")
    private int cookieMaxAge;

    @RequestMapping("/login")
    public Result login(String username, String password, HttpServletResponse response){
        if(StringUtils.isEmpty(username)){
            throw new RuntimeException("用户名不允许为空");
        }
        if(StringUtils.isEmpty(password)){
            throw new RuntimeException("密码不允许为空");
        }
        AuthToken authToken = authService.login(username, password, clientId, clientSecret);
        String accessToken = authToken.getAccessToken();

        CookieUtil.addCookie(response,cookieDomain,"/","Authorization",accessToken,cookieMaxAge,false);
        return new Result(true, StatusCode.OK,"登录成功");
    }
}
  1. service层实现
@Service
public class AuthServiceImpl implements AuthService {

    @Autowired
    RestTemplate restTemplate;

    @Autowired
    LoadBalancerClient loadBalancerClient;

    /**
     * 密码登录模式
     * @param username      用户名
     * @param password      用户密码
     * @param clientId      客户端id
     * @param clientSecret  客户端密钥
     * @return
     */
    @Override
    public AuthToken login(String username, String password, String clientId, String clientSecret) {
        //获取请求地址   http://localhost:9001/oauth/token
        //选中认证服务的地址
        ServiceInstance serviceInstance = loadBalancerClient.choose("user-auth");
        if(serviceInstance==null){
            throw new RuntimeException("找不到对应的微服务");
        }
        //获取令牌的url
        String path = serviceInstance.getUri().toString() + "/oauth/token";

        //定义body
        MultiValueMap<String, String> bodyMap = new LinkedMultiValueMap<>();
        bodyMap.add("grant_type","password");
        bodyMap.add("username",username);
        bodyMap.add("password",password);
        //定义头
        MultiValueMap<String, String> headerMap = new LinkedMultiValueMap<>();
        //头格式为Basic +
        //               clientId:clientSecret的Base65转码后的结果
        String string = clientId+":"+clientSecret;
        byte[] encode = Base64Utils.encode(string.getBytes());
        String headerStr = "Basic "+new String(encode);
        headerMap.add("Authorization",headerStr);

        restTemplate.setErrorHandler(new DefaultResponseErrorHandler() {
            @Override
            public void handleError(ClientHttpResponse response) throws IOException {
                //当响应的值为400或401时候也要正常响应,不要抛出异常
                if (response.getRawStatusCode() != 400 && response.getRawStatusCode() != 401) {
                    super.handleError(response);
                }
            }
        });

        //向服务端发送请求
        //http请求spring security的申请令牌接口
        Map map=null;
        ResponseEntity<Map> mapResponseEntity = restTemplate.exchange(path, HttpMethod.POST,
                                                new HttpEntity<MultiValueMap<String, String>>(bodyMap, headerMap), Map.class);
        map = mapResponseEntity.getBody();

        if(map==null || map.get("access_token")==null || map.get("refresh_token")==null || map.get("jti")==null){
            throw new RuntimeException("创建令牌失败");
        }
        AuthToken authToken = new AuthToken();
        authToken.setAccessToken(map.get("access_token").toString());
        authToken.setRefreshToken(map.get("refresh_token").toString());
        authToken.setJti(map.get("jti").toString());
        return authToken;
    }
  1. SpringSecurityOauth的一些其他配置,此处省略

5.oauth2.0微服务之间的认证

1.认证流程

1.用户在访问oauth微服务时,比如说在进行登录时,肯定会访问user微服务,调用findByUserId方法查询用户名是否存在
2.这时候就会存在一个微服务之间的认证问题,user微服务会校验oauth微服务的feign调用是否还有token,以及token的格式,权限,但是用户此时还未登录
	没有token令牌
3.由于是自己系统内部的微服务互相调用,此时可以设置一个feign调用拦截器,手动生成一个token,设置权限为admin后,在进行feign调用
4.user微服务收到请求后,校验token,根据id查询对应的用户信息并返回。
5.若是访问其他微服务,如果用户已登录,直接在feign调用之前获取请求头文件,将其中的内容存到requestTemplate的头文件中去

2.详细步骤

1.登录请求

  1. 定义一个拦截器
    在项目目录下创建interceptor包,并创建OauthInterceptor类
   public class OauthInterceptor implements RequestInterceptor {
   @Override
   public void apply(RequestTemplate requestTemplate) {
       AdminToken adminToken = new AdminToken();
       String token = "bearer "+adminToken.createAdminToken();
       requestTemplate.header("Authorization",token);
   }
}

2.当中的AdminToken是一个工具类,专门为访问user微服务远程调用时,因为是登录操作,还没有携带token,又需要根据用户名查询用户信息,所以手动创建一个token,存入到头文件中
代码如下:

public String createAdminToken(){
        //证书文件路径
        String key_location="changgou.jks";
        //密钥库密码
        String key_password="changgou";
        //密钥密码
        String keypwd = "changgou";
        //密钥别名
        String alias = "changgou";

        //加载证书文件
        ClassPathResource resource = new ClassPathResource(key_location);
        //根据证书文件创建密钥工厂
        KeyStoreKeyFactory keyStoreKeyFactory = new KeyStoreKeyFactory(resource, key_password.toCharArray());

        //读取密钥对(公钥,私钥)
        KeyPair keyPair = keyStoreKeyFactory.getKeyPair("changgou");

        //获取私钥
        RSAPrivateKey rasPrivate = (RSAPrivateKey) keyPair.getPrivate();

        //定义payload(载荷)
        HashMap<String, Object> tokenMap = new HashMap<>();
        tokenMap.put("id",1);
        tokenMap.put("name","itheima");
        //登录调用是内部调用,直接给与admin泉休闲
        tokenMap.put("authorities",new String[]{"admin","oauth"});

        //生成Jwt令牌
        Jwt jwt = JwtHelper.encode(JSON.toJSONString(tokenMap), new RsaSigner(rasPrivate));

        //取出令牌
        String encoded = jwt.getEncoded();
        //System.out.println(encoded);

        return encoded;
    }

2.其他请求

拦截器内容如下:

public class FeignInterceptor implements RequestInterceptor{

    /**
     * 每次微服务调用之前,都先检查头文件
     * 并将头文件中的令牌数据,存放到header中
     * @param requestTemplate
     */
    @Override
    public void apply(RequestTemplate requestTemplate) {

        //使用RequestContextHolder工具获取request相关变量
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        //判断是否为空
        if(attributes!=null){
            Enumeration<String> headerNames = attributes.getRequest().getHeaderNames();
            if(headerNames!=null){
                while (headerNames.hasMoreElements()){
                    String headerName = headerNames.nextElement();
                    String values = attributes.getRequest().getHeader(headerName);
                    requestTemplate.header(headerName,values);
                }
            }
        }
        //遍历,加入到新的header中

    }
}

其他操作类似
详情见本地文档

标签:令牌,springboot,jwt,token,new,Oauth2.0,public,changgou,String
来源: https://blog.csdn.net/weixin_45141938/article/details/106417473