Spring Security+Oauth2+JWT的使用

技术相关 浏览量: 188 作者: 谁的猫 2020-08-07

这次抽空好好理解了一下java中关于oauth2的使用,及结合zuul网关实现的一些方式,稍微记录一下,省得以后忘记了

 

Oauth2认证流程

网上偷了一张图,显示了具体的实现方式

说实话  这张图意思还是比较明显了

登录login--> 走网关(网关放行)--> 直接到auth验证服务器 -->认证完成,把令牌放到cookie,顺便redis存一份,同时拿到客户信息

然后如果再访问其他的微服务,网关拦截,然后验证cookie中的令牌,同时跟redis中核对,并且令牌要携带上

 

我这里选的是JWT令牌

这里就不讲JWT的结构了 大概就是 头部载荷签名三部分 具体详情可以百度

如果想自己生成秘钥证书,可以用keytool

如果你有配置java环境变量,那么cmd直接输入(生成的文件就在你的当前文件夹下)

keytool -genkeypair -alias xhwlxhwl -keyalg RSA -keypass xhwlxhwl -keystore xhwl.jks -storepass xhwlxhwl

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

查询证书信息:

keytool -list -keystore xhwl.jks

导出公钥

用openssl

安装 openssl:点击下载

配置openssl的path环境变量 配置到bin目录

然后再xhwl.jks的文件所在目录执行

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

输入口令,我刚才设置的是xhwlxhwl

长这样

公钥内容从 -----BEGIN PUBLIC KEY----- 到 -----END PUBLIC KEY-----

复制下来

新建一个文本  粘贴进去  把文件名字改为 public.key(最好把所有内容粘贴成一行)

这样公钥就有了

下面上代码

下载代码

或者直接git(git的话有其他的微服务在里面,不太适合学习)

https://github.com/Yoki-Hua/xhiot.git

数据库新建一张表

/*
Navicat MySQL Data Transfer

Source Server         : Huaz
Source Server Version : 50730
Source Host           : 127.0.0.1:3306
Source Database       : xhwl_user

Target Server Type    : MYSQL
Target Server Version : 50730
File Encoding         : 65001

Date: 2020-08-12 11:27:39
*/

SET FOREIGN_KEY_CHECKS=0;

-- ----------------------------
-- Table structure for oauth_client_details
-- ----------------------------
DROP TABLE IF EXISTS `oauth_client_details`;
CREATE TABLE `oauth_client_details` (
  `client_id` varchar(48) NOT NULL COMMENT '客户端ID,主要用于标识对应的应用',
  `resource_ids` varchar(256) DEFAULT NULL,
  `client_secret` varchar(256) DEFAULT NULL COMMENT '客户端秘钥,BCryptPasswordEncoder加密',
  `scope` varchar(256) DEFAULT NULL COMMENT '对应的范围',
  `authorized_grant_types` varchar(256) DEFAULT NULL COMMENT '认证模式',
  `web_server_redirect_uri` varchar(256) DEFAULT NULL COMMENT '认证后重定向地址',
  `authorities` varchar(256) DEFAULT NULL,
  `access_token_validity` int(11) DEFAULT NULL COMMENT '令牌有效期',
  `refresh_token_validity` int(11) DEFAULT NULL COMMENT '令牌刷新周期',
  `additional_information` varchar(4096) DEFAULT NULL,
  `autoapprove` varchar(256) DEFAULT NULL,
  PRIMARY KEY (`client_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

-- ----------------------------
-- Records of oauth_client_details
-- ----------------------------
INSERT INTO `oauth_client_details` VALUES ('xhwl', null, '$2a$10$digQWwOMjrH6VEgdJnNN2uLR2uqkjIzCOJUK0wVNZ5CmPqyrcmCxe', 'app', 'authorization_code,password,refresh_token,client_credentials', 'http://localhost', null, '3600', '43200', null, null);

这个客户端密钥加密了  明文是xhwl

如果你打开了代码

下面直接从代码开始讲解,就不介绍那些所谓的授权码模式了

直接上密码模式,密码模式你得先有一张user表,自己建一个就行如果不想建表(测试时建议这样,不然你还得去写一个查询数据库的方法,我这里不写了,总代码里面有),就这么写,放开注释,把密码定死把下面两行注释掉

postman请求

http://localhost:9003/oauth/token

携带参数: 
grant_type:密码模式授权填写password 
username:账号 
password:密码 

然后配置Authorization

下面看登录是怎样实现的,理解登陆过程详情你就理解了oauth2了

下面只讲代码  不讲配置  配置没啥好说的  就是一些定死的东西  读出来的而已

简单的登录controller  中间调用service   路径是/oauth/login  参数是 username  password 

service实现类是重点,看注释吧

public AuthToken applyToken(String username, String password, String clientId, String clientSecret) {

        //1.申请令牌
//                                         这里的意思是选择“auth-service”这个微服务的地址
//                     也就是http://localhost:9003
        ServiceInstance serviceInstance = loadBalancerClient.choose("auth-service");

        URI uri = serviceInstance.getUri();
//那么这个url  就是  http://localhost:9003/oauth/token   是不是很眼熟?就是oauth2的获取token那个
//你也可以直接让前端调用那个接口也行  这里不是那么做的   以这里为准
        String url = uri + "/oauth/token";
//就是new一个MultiValueMap 就当他是一个Map吧
        MultiValueMap<String, String> body = new LinkedMultiValueMap<>();
        body.add("grant_type", "password");
        body.add("username", username);
        body.add("password", password);
//里面存的参数
//这里又新建了一个Map  我也不想啊
        MultiValueMap<String, String> headers = new LinkedMultiValueMap<>();
//把头存进去            getHttpBasic这是个方法  在下面
        headers.add("Authorization", this.getHttpBasic(clientId, clientSecret));
        HttpEntity<MultiValueMap<String, String>> requestEntity = new HttpEntity<>(body, headers);
//这个不管  可以删掉  这是一个判断  加强逻辑
        restTemplate.setErrorHandler(new DefaultResponseErrorHandler() {
            @Override
            public void handleError(ClientHttpResponse response) throws IOException {
                if (response.getRawStatusCode() != 400 && response.getRawStatusCode() != 401) {
                    super.handleError(response);
                }
            }
        });
        //发送请求   就相当于是你在postman上发请求  只不过变成了java来发  用Map接收
        ResponseEntity<Map> responseEntity = restTemplate.exchange(url, HttpMethod.POST, requestEntity, Map.class);
//返回的值
        Map map = responseEntity.getBody();
        if (map == null || map.get("access_token") == null || map.get("refresh_token") == null || map.get("jti") == null) {
            //申请令牌失败
            throw new RuntimeException("申请令牌失败");
        }

        //2.封装结果数据,把取出来的数据存到一个对象里面去  这个对象要自己建  
        AuthToken authToken = new AuthToken();
        authToken.setAccessToken((String) map.get("access_token"));
        authToken.setRefreshToken((String) map.get("refresh_token"));
        authToken.setJti((String) map.get("jti"));

        //3.将jti作为redis中的key,将jwt作为redis中的value进行数据的存放  我存的是jti  对应  authtoken对象
        stringRedisTemplate.boundValueOps(authToken.getJti()).set(JsonUtils.toString(authToken), ttl, TimeUnit.SECONDS);
        return authToken;
    }

private String getHttpBasic(String clientId, String clientSecret) {

        String value = clientId + ":" + clientSecret;
        byte[] encode = Base64Utils.encode(value.getBytes());
        return "Basic " + new String(encode);
    }

这就是整个登陆的逻辑了  对了  在controller中还要把jti加到cookie中

没看见在哪有用到密码比对?记得这个类吗  其实在这里

那里把密码写死了,只要密码是xhwl就行   账号无所谓

下面   连接到网关  通过网关来做一个拦截

由于我是zuul网关  所以自定义一个拦截器  继承zuulFilter

看注释

package com.xhwl.filter;

import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import com.xhwl.interfaceClient.*;
import com.xhwl.config.FilterProperties;
import com.xhwl.pojo.AuthToken;
import com.xhwl.service.AuthService;
import com.xhwl.utils.CookieUtil;
import org.apache.commons.lang.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.cloud.netflix.zuul.filters.support.FilterConstants;

import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Component;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.List;

@Component
@EnableConfigurationProperties(FilterProperties.class)
public class AuthFilter extends ZuulFilter {
    @Autowired
    private FilterProperties filterProps;
    @Autowired
    private AuthService authService;
    @Autowired
    private AuthClient authClient;
    @Value("${xh.cookieDomain}")
    private String cookieDomain;

    @Override
    public String filterType() {
        return FilterConstants.PRE_TYPE;
    }

    @Override
    public int filterOrder() {
        return FilterConstants.FORM_BODY_WRAPPER_FILTER_ORDER - 1;
    }
    /**
     * true过滤器生效
     * false过滤器不生效
     *
     *
     */
    @Override
    public boolean shouldFilter() {
        RequestContext currentContext = RequestContext.getCurrentContext();

        HttpServletRequest request = currentContext.getRequest();

        //获取请求资源地址
        String requestURI = request.getRequestURI();

        //获取白名单  这些配置在application中
        List<String> allowPaths = filterProps.getAllowPaths();

        //如果资源白名单,出现在资源地址中,则放行,不是,则拦截
        for (String allowPath : allowPaths) {
            if (requestURI.startsWith(allowPath)){
                return false;
            }
        }

        return true;
    }
    /**
     * 校验登录状态,从cookie中获取jti的值
     * 从redis中获取token 插入header中
     * @return
     * @throws
     */
    @Override
    public Object run()  {
        //拦截后  初始化一个RequestContext
        RequestContext currentContext = null;
        currentContext  = RequestContext.getCurrentContext();
        //获取request和response
        HttpServletRequest request = currentContext.getRequest();
        HttpServletResponse response = currentContext.getResponse();
        //由于我们存的是jti  所以取jti
        //2.从cookie中获取jti的值,如果该值不存在,说明没有token ,拒绝本次访问
        String jti = authService.getJtiFromCookie(request);
        if (StringUtils.isEmpty(jti)){

            currentContext.setResponseStatusCode(HttpServletResponse.SC_UNAUTHORIZED);
            currentContext.setSendZuulResponse(false);
            currentContext.setResponseBody("NO TOKEN!!!");
            return null;
        }
        //因为我们往redis也存了一份  所以要通过cookie取出来的jti取redis查有没有对应的token
        //3.从redis中获取jwt的值,如果该值不存在,说明已过期,重新刷新token
        String jwt = authService.getJwtTokenFromRedis(jti);
        if (StringUtils.isEmpty(jwt)) {
            currentContext.setResponseStatusCode(HttpServletResponse.SC_UNAUTHORIZED);
            currentContext.setSendZuulResponse(false);
            currentContext.setResponseBody("TOKEN INVALID!!!");
            return null;
        } else {
            //就算有token  也要判断token还剩多久的有效期
            Long jwtTime = authService.getJwtTimeFromRedis(jti);
            //如果jwtTime时间<30分钟(1800秒),刷新,重新拿个新的token,大于不管
            if (jwtTime < 1800) {
                String refreshToken = authService.getJwtRefreshTokenFromRedis(jti);
                ResponseEntity<AuthToken> token = authClient.refreshToken("refresh_token", refreshToken, jti);
                jwt = token.getBody().getAccessToken();
                //更新浏览器的cookie
                CookieUtil.addCookie(response,cookieDomain,"/","uid",token.getBody().getJti(),-1,false);
            }

        }

        //4.header携带令牌的信息 继续传递到微服务  微服务才能通过验证查询

        currentContext.addZuulRequestHeader("Authorization","Bearer "+jwt);
        return null;
    }
}

 

这么做其实相对来说比较麻烦  但是保证了token安全性,因为token一旦颁发,没有过期之前,都是有效的,哪怕你刷新了token  旧的token还是有效,

我们用redis做二次校验,刷新token后原来的token会从redis删除掉,这样每一次来的请求都会先去看redis里是否存在  所以旧的token是找不到的,从网关走就无法走通

 

emm....感觉讲的不是很清楚,但是代码已经贴上了  看源码吧   反正我算是跑通了的  可能代码写的很垃圾,但是功能实现了(狗头保命)

OVER!

 

Top