栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > Java

Oauth2 认证

Java 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

Oauth2 认证

1 简介 1.1 基本概念

认证:用户访问系统资源时系统要求验证用户的身份信息,身份合法方可继续访问,常见的账号密码登录,验证码登录,指纹登陆授权:用户认证通过后去访问系统的资源,系统会判断用户是否拥有访问资源的权限单点登录 SSO:用户在一个系统中登录,其他任意受信任的系统都可以访问,例如在京东主页登陆了,京东其他页面就不需要再登录,这个功能就叫单点登录。第三方账号登录:第三方系统对用户认证通过 1.2 认证解决方案 1.2.1 单点登录技术方案

分布式系统要实现单点登录,通常将认证系统独立抽取出来,并且将用户身份信息存储在单独的存储介质,比如:MySQL、Redis,考虑性能要求,通常存储在Redis中

Java中有很多用户认证的框架都可以实现单点登录:

shiroCASSpring security CAS 1.2.2 第三方登录技术方案 1.2.2.1 2.2.1 Oauth2认证

OAUTH协议为用户资源的授权提供了一个安全的、开放而又简易的标准。同时,任何第三方都可以使用OAUTH认 证服务,任何服务提供商都可以实现自身的OAUTH认证服务,因而OAUTH是开放的

例如qq音乐登录微信授权的过程:

2 jwt令牌 2.1 令牌组成

JWT令牌由三部分组成,每部分中间使用点(.)分隔,比如:xxxxx.yyyyy.zzzzz

2.1.1 Header

头部包括令牌的类型(即JWT)及使用的哈希算法(如HMAC SHA256或RSA)

{
    "alg": "HS256",
    "typ": "JWT"
}

将上边的内容使用base64Url编码,得到一个字符串就是JWT令牌的第一部分。

2.1.2 Payload

第二部分是荷载,内容也是一个json对象,它是存放有效信息的地方,它可以存放jwt提供的现成字段,比 如:iss(签发者),exp(过期时间戳), sub(面向的用户)等,也可自定义字段。

此部分不建议存放敏感信息,因为此部分可以解码还原原始内容。

最后将第二部分负载使用base64Url编码,得到一个字符串就是JWT令牌的第二部分。
例如

{
    "sub": "1234567890",
    "name": "张三",
    "admin": true
}
2.1.3 Signature

第三部分是签名,此部分用于防止jwt内容被篡改。

这个部分使用base64url将前两部分进行编码,编码后使用点(.)连接组成字符串,最后使用header中声明 签名算法进行签名。

secret:签名所使用的密钥。

HMACSHA256(
    base64UrlEncode(header) + "." +
    base64UrlEncode(payload),
    secret)
2.2 生成私钥公钥-运维

JWT令牌生成采用非对称加密算法

2.2.1 生成密钥证书
keytool -genkeypair -alias xiaomanhms -keyalg RSA -keypass xiaomanhms -keystore xiaomanhms.jks -storepass xiaomanhms


查询证书信息:

2.2.2 导出公钥

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

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

cmd进入xiaomanhms.jks文件所在目录执行如下命令:

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

公钥部分为

-----BEGIN PUBLIC KEY-----MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAighoxR7gI5en9bGiTW/61mENj6Oy4RysI6yO6MTFdFoEpYNJUMxrarfT4baOGuKvFg5iAW6pSDDMzHK7Dy9EeOI2JAETLVC82HwJ2NQNDvzBzJ1mf16V11I+PI+ZbhsiLn3PJsm42Egvgktygf9TTIxd879etJR89vwEnmVSrVwYvhD4e0BkaL0gB0oOs8o431DY6SKPYoAOichdue8uB7gtn4UhvX5dJiCvKByknQTspHHp1Ufdc4obclWnxDv/h0OfRGOYO2itQPm/iKUZBY9VFRxKanG2r+8kCLCFwmRYnng/WkbJRXf+O8gVmftSFCfMlSvPqHzTdGPvz9M90wIDAQAB-----END PUBLIC KEY-----

将上边的公钥拷贝到文本public.key文件中,合并为一行

2.2.3 基于私钥生成jwt令牌-了解 2.2.3.1 导入认证服务

将hms_user_auth的工程导入到项目中去


然后在父工程pom文件中加入

hms-user-oauth
2.2.3.2 认证服务中创建测试类

用私钥加密

public class CreateJWTTest {
    public static void main(String []args) {
        // 1 创建秘钥工厂
        ClassPathResource classPathResource = new ClassPathResource("xiaomanhms.jks");
        // 2 秘钥库密码
        String keyPass = "xiaomanhms";
        
        KeyStoreKeyFactory keyStoreKeyFactory = new KeyStoreKeyFactory(classPathResource, keyPass.toCharArray());

        // 2基于工厂拿到私钥
        String alias="xiaomanhms";
        String password="xiaomanhms";
        KeyPair keyPair = keyStoreKeyFactory.getKeyPair(alias, password.toCharArray());

        //转化为rsa私钥
        RSAPrivateKey rsaPrivateKey = (RSAPrivateKey)keyPair.getPrivate();

        // 3生成jwt
        Map map = new HashMap<>();
        map.put("conpany", "hbue");
        map.put("address", "wuhan");
        Jwt jwt = JwtHelper.encode(JSON.toJSONString(map), new RsaSigner(rsaPrivateKey));

        String jwtEncoded = jwt.getEncoded();
        System.out.println("jwtEncoded:"+jwtEncoded);
        String claims = jwt.getClaims();
        System.out.println("claims:"+claims);

    }
}

公钥解密

public class ParseJwtTest {
    public static void main(String[] args) {
        //基于公钥去解析jwt
        String jwt = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJhZGRyZXNzIjoid3VoYW4iLCJjb25wYW55IjoiaGJ1ZSJ9.Zmo4igopc9mKBW4BnbIms4ux4fDq80iV0PvPbwegr9kWT_JP129WCVHn0BkXHTTfMGSmBAwg9XdzvldbWlgTn-IfLPFYfsRFsunwBxMMkqVi4R5IsHczYu-Js2Bg6lZpHwRlbjmyQU0TkzM6bJPnTIxhaxLto7OuLwpSrG27NZUC8BH3BSJPzIp-fzw9NJNrskdt9UWyHcuGmw0dmrFvNKvQ64vQE6_ns0tGHv1vNkkaJdsRbPKVQw-0JpXEbQOVrM3iTdBwbqqXzEkm5fpFEQJzT-SvRkJdRyulvEmS4XL4iO6qzQ3zRLDUoOhV5f3ApUSGfPExtexTcilLTFkiQA";
        // 公钥
        String publicKey = "-----BEGIN PUBLIC KEY-----MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAighoxR7gI5en9bGiTW/61mENj6Oy4RysI6yO6MTFdFoEpYNJUMxrarfT4baOGuKvFg5iAW6pSDDMzHK7Dy9EeOI2JAETLVC82HwJ2NQNDvzBzJ1mf16V11I+PI+ZbhsiLn3PJsm42Egvgktygf9TTIxd879etJR89vwEnmVSrVwYvhD4e0BkaL0gB0oOs8o431DY6SKPYoAOichdue8uB7gtn4UhvX5dJiCvKByknQTspHHp1Ufdc4obclWnxDv/h0OfRGOYO2itQPm/iKUZBY9VFRxKanG2r+8kCLCFwmRYnng/WkbJRXf+O8gVmftSFCfMlSvPqHzTdGPvz9M90wIDAQAB-----END PUBLIC KEY-----";

        //解析令牌
        Jwt token = JwtHelper.decodeAndVerify(jwt, new RsaVerifier(publicKey));
        //获取负载
        String claims = token.getClaims();
        System.out.println(claims);
    }
}

3 Oauth2.0 3.1 准备工作

导入表oauth_client_details

oauth框架必须有的表

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;

导入1条初始化数据,其中加密字符明文为xiaoman

INSERT INTO `oauth_client_details` VALUES ('userweb', NULL, '$2a$10$ejwy6rInOvFWF44gAAgpA.iPWNhmVo7WSG98Yvg4lY9gCmVhxkNRa', 'app', 'authorization_code,password,refresh_token,client_credentials', 'http://localhost', NULL, 43200, 43200, NULL, NULL);

3.2 Oauth2授权模式介绍

OAuth2.0协议一共支持 4 种不同的授权模式:

授权码模式:常见的第三方平台登录功能基本都是使用这种模式。简化模式:简化模式是不需要客户端服务器参与,直接在浏览器中向授权服务器申请令牌(token),一般如果网站是纯静态页面则可以采用这种方式。密码模式:密码模式是用户把用户名密码直接告诉客户端,客户端使用说这些信息向授权服务器申请令牌(token)。这需要用户对客户端高度信任,例如客户端应用和服务提供商就是同一家公司,自己做前后端分离登录就可以采用这种模式。客户端模式:客户端模式是指客户端使用自己的名义而不是用户的名义向服务提供者申请授权,严格来说,客户端模式并不能算作 OAuth 协议要解决的问题的一种解决方案,但是,对于开发者而言,在一些前后端分离应用或者为移动端提供的认证授权服务器上使用这种模式还是非常方便的。

一般用的比较多的就是 授权码模式和密码模式

3.2.1 授权码模式 3.2.1.1 授权码授权流程

3.2.1.2 申请授权码

启动hms_user_oauth微服务

浏览器访问

http://localhost:9200/oauth/authorize?client_id=userweb&response_type=code&scop=app&redirect_uri=http://localhost

client_id为username, client_secret为password

登陆后会进入进入授权页面:


点击Authorize,相当于我们微信登录qq音乐扫码后点击确认登陆,接下来返回授权码: 认证服务携带授权码跳转redirect_uri,code=9prf3B就是返回的授权码, 每一个授权码只能使用一次

注意观察:此接口为oauth2提供,项目中没有写controller。

3.2.1.3 申请令牌

拿到授权码后,申请令牌。

http://localhost:9200/oauth/token

参数

grant_type:授权类型,填写authorization_code,表示授权码模式
code:授权码,就是刚刚获取的授权码,注意:授权码只使用一次就无效了,需要重新申请。
redirect_uri:申请授权码时的跳转url,一定和申请授权码时用的redirect_uri一致。

此链接需要使用 http Basic认证。

什么是http Basic认证?

http协议定义的一种认证方式,将客户端id和客户端密码按照“客户端ID:客户端密码”的格式拼接,并用base64编 码,放在header中请求服务端,一个例子:

Authorization:Basic WGNXZWJBcHA6WGNXZWJBcHA=WGNXZWJBcHA6WGNXZWJBcHA= 是用户名:密码的base64编码。 认证失败服务端返回 401 Unauthorized。

以上测试使用poatman(apifox没成功)完成:

http basic认证:


点击发送: 申请令牌成功

3.2.1.3 校验令牌

Get:http://localhost:9200/oauth/check_token?token= [access_token]

3.2.1.4 刷新令牌

刷新令牌是当令牌快过期时重新生成一个令牌,它与授权码授权和密码授权生成令牌不同,刷新令牌不需要授权码 也不需要账号和密码,只需要一个刷新令牌、客户端id和客户端密码。

Post:http://localhost:9200/oauth/token

grant_type: 固定为 refresh_token

refresh_token:刷新令牌

3.2.2 密码模式

密码模式(Resource Owner Password Credentials)与授权码模式的区别是申请令牌不再使用授权码,而是直接 通过用户名和密码即可申请令牌

3.2.2.1 申请授权码

http://localhost:9200/oauth/token

此链接需要使用 http Basic认证

下面是测试数据

3.2.3 资源服务授权 3.2.3.1 流程

资源服务拥有要访问的受保护资源,客户端携带令牌访问资源服务,如果令牌合法则可成功访问资源服务中的资源,如下图:

3.2.3.2 改造用户服务 对接Oauth2

1、配置公钥 ,将 hms_user_auth 项目中public.key复制到hms_service_user中

2、添加依赖

        
            org.springframework.cloud
            spring-cloud-starter-oauth2
        
        

3、配置每个系统的Http请求路径安全控制策略以及读取公钥信息识别令牌

@Configuration
@EnableResourceServer
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)//激活方法上的PreAuthorize注解
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
    
    private static final String PUBLIC_KEY = "public.key";

    
    @Bean
    public TokenStore tokenStore(JwtAccessTokenConverter jwtAccessTokenConverter) {
        return new JwtTokenStore(jwtAccessTokenConverter);
    }

    
    @Bean
    public JwtAccessTokenConverter jwtAccessTokenConverter() {
        JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
        converter.setVerifierKey(getPubKey());
        return converter;
    }
    
    private String getPubKey() {
        Resource resource = new ClassPathResource(PUBLIC_KEY);
        try {
            InputStreamReader inputStreamReader = new InputStreamReader(resource.getInputStream());
            BufferedReader br = new BufferedReader(inputStreamReader);
            return br.lines().collect(Collectors.joining("n"));
        } catch (IOException ioe) {
            return null;
        }
    }

    
    @Override
    public void configure(HttpSecurity http) throws Exception {
        //所有请求必须认证通过
        http.authorizeRequests()
                //下边的路径放行
                .antMatchers(
                        "/user/add","/user/load
    @GetMapping("user/findByAccount/{account}")
    public Result findByAccount(@PathVariable("account") String account);
}
5.4.3 认证服务导入user-api依赖
	
          com.wang
          hms_service_user_api
          1.0-SNAPSHOT
      
5.4.4 认证启动类添加注解
@EnableFeignClients(basePackages = {"com.wang.feign"})
5.4.5 修改UserDetailsServiceImpl 使之动态获取用户信息
@Service
public class UserDetailsServiceImpl implements UserDetailsService {

    @Autowired
    private ClientDetailsService clientDetailsService;

    @Autowired
    private UserFeign userFeign;

    
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        //取出身份,如果身份为空说明没有认证
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        //没有认证统一采用httpbasic认证,httpbasic中存储了client_id和client_secret,开始认证client_id和client_secret
        if(authentication==null){
            ClientDetails clientDetails = clientDetailsService.loadClientByClientId(username);
            if(clientDetails!=null){
                //秘钥
                String clientSecret = clientDetails.getClientSecret();
                //静态方式
                //return new User(username,new BCryptPasswordEncoder().encode(clientSecret), AuthorityUtils.commaSeparatedStringToAuthorityList(""));
                // 数据库查找方式
                return new User(username,clientSecret, AuthorityUtils.commaSeparatedStringToAuthorityList(""));
            }
        }

        if (StringUtils.isEmpty(username)) {
            return null;
        }

        //根据用户名查询用户信息
        com.wang.entity.User user = userFeign.findByAccount(username);
        //创建User对象
        String permissions = "goods_list,seckill_list";
        UserJwt userDetails = new UserJwt(username, user.getPassword(), AuthorityUtils.commaSeparatedStringToAuthorityList(permissions));
        return userDetails;
    }
}

5.4.6用户服务ResourceServerConfig类的configure方法

/user/findByAccount接口放行

5.4.7 重启测试

6 认证服务对接网关

为什么有系统网关还要web网关呢?

系统网关是让别的系统调用我的系统,比如引流,web网关是用户调用

6.1 新建网关工程ydles_gateway_web

依赖

    
        
            io.jsonwebtoken
            jjwt
            0.9.0
        
        
        
            org.springframework.boot
            spring-boot-starter-data-redis-reactive
        

        
            org.springframework.cloud
            spring-cloud-starter-gateway
        

        
        
            com.alibaba.cloud
            spring-cloud-starter-alibaba-nacos-discovery
        

        
            org.projectlombok
            lombok
        

        
            com.wang
            hms_common
            1.0-SNAPSHOT
            
                
                    org.springframework.boot
                    spring-boot-starter-web
                
            
        
    

配置文件

server:
  port: 11000
spring:
  application:
    name: webgatway
  cloud:
    nacos:
      server-addr: localhost:8848
    gateway:
      routes: # 网关路由配置
        - id: hospital # 路由id,自定义,只要唯一即可
          uri: lb://hospital # 路由的目标地址 lb就是负载均衡,后面跟服务名称
          predicates: # 路由断言,也就是判断请求是否符合路由规则的条件
            - Path=/hosp
@Component
public class AuthFilter implements GlobalFilter, Ordered {
    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    @Override
    public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        ServerHttpRequest request = exchange.getRequest();
        ServerHttpResponse response = exchange.getResponse();

        String path = request.getURI().getPath();
        if(path.endsWith("/oauth/login")) {
            return chain.filter(exchange);
        }
        // 判断cookie中有没有jti
        Httpcookie cookie = request.getcookies().getFirst("jti");
        if(cookie == null) {
            response.setStatusCode(HttpStatus.UNAUTHORIZED);
            return response.setComplete();
        }
        String jti = cookie.getValue();

        String jwt = stringRedisTemplate.boundValueOps(jti).get();
        if(StringUtils.isEmpty(jwt)){
            //拒绝访问
            response.setStatusCode(HttpStatus.UNAUTHORIZED);
            return response.setComplete();
        }
        // 对当前请求增强,让其携带令牌信息(转发)
        request.mutate().header("Authorization", "Bearer " + jwt);
        return chain.filter(exchange);
    }

    @Override
    public int getOrder() {
        return 0;
    }
}

6.2.3 测试

启动gatewayweb

测试登录:http://localhost:11000/api/oauth/login

测试查询用户:http://localhost:11000/api/user/userList
查询成功,查看cookie中有jti

转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/771206.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号