OAuth2.0

OAuth(Open Authorization)是一个关于授权(authorization)的开放网络标准,允许用户授权第三方应用访问他们存储在另外的服务提供者上的信息,而不需要将用户名和密码提供给第三方移动应用或分享他们数据的所有内容。OAuth在全世界得到广泛应用,目前的版本是2.0版。
OAuth协议:https://tools.ietf.org/html/rfc6749
特点:

  • 简单:OAuth服务提供还是应用开发者都易理解使用
  • 安全:没有涉及到用户密钥等信息
  • 开放:任何服务商都可以实现OAuth,任何软件开发商都可以使用OAuth

    应用场景

  • 原生app授权:app登录请求后台接口,为了安全认证,所有请求需要有token信息。

  • 前后端分离单页面应用:前后端分离框架。
  • 第三方应用授权:QQ、微信等。

    基本概念

  1. Third-party application:第三方应用程序,又称“客户端”。
  2. HTTP service:HTTP服务提供商,简称“服务提供商”。
  3. Resource Owner:资源所有者,又称“用户”。
  4. User Agent:用户代理,比如浏览器。
  5. Authorization server:授权服务器,即服务提供商专门用来处理认证授权的服务器。
  6. Resource server:资源服务器,即服务提供商存放用户生成的资源的服务器。它与授权服务器,可以是同一台服务器,也可以是不同的服务器。

OAuth的作用就是让“客户端”安全可控的获取“用户”的授权,与“服务提供商”进行交互。

优缺点

优点:

  • 安全
  • 广泛采用
  • 短寿命封装token
  • 资源服务器和授权服务器解耦
  • 集中式授权
  • 易于请求传递token
  • 考虑多种客户端场景
  • 客户可以有不同的新人级别

缺点:

  • 协议太宽泛,兼容性和互操性差
  • 不是一个认证协议,本身不能告诉你任何用户信息

    OAuth2设计思路

  1. 用户打开客户端后,客户端要求用户授权
  2. 用户同意给客户端授权
  3. 客户端使用获得授权,像授权服务器申请令牌
  4. 授权服务器对客户端进行认证后,确认无误,同意发放令牌
  5. 客户端使用令牌,想资源服务器申请获取资源
  6. 资源服务器确认令牌无误,同意发放资源

    客户端授权模式

  • 授权码模式
    • 用户访问客户端,后者将前者导向授权客户端
    • 用户选择是否给与客户端授权
    • 假设用户同意授权,授权服务器将用户导向客户端事先指定的重定向URI,同时附上一个授权码
    • 客户端收到授权码,附上重定向URI,向授权服务器申请令牌。(在客户端的后台服务器完成)
    • 授权服务器核对授权码,并重定向URI。确认无误后,向客户端发送访问令牌和更新令牌
  • 密码模式
  • 简化模式
  • 客户端模式

    快速开始

    配置

    pom

    1. <dependency>
    2. <groupId>org.springframework.cloud</groupId>
    3. <artifactId>spring-cloud-starter-oauth2</artifactId>
    4. </dependency>

    security配置

    @Configuration
    public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    
      @Autowired
      private UserService userService;
    
      @Bean
      public PasswordEncoder passwordEncoder() {
          return new BCryptPasswordEncoder();
      }
    
      @Override
      public void configure(AuthenticationManagerBuilder auth) throws Exception {
          //获取用户信息
          auth.userDetailsService(userService);
      }
    
      @Override
      protected void configure(HttpSecurity http) throws Exception {
          http.formLogin().permitAll()
                  .and().authorizeRequests()
                  .antMatchers("/oauth/**").permitAll()
                  .anyRequest().authenticated()
                  .and().logout().permitAll()
                  .and().csrf().disable();
      }
    
      //@Bean
      //public UserDetailsService userDetailsService() {
      //    InMemoryUserDetailsManager inMemoryUserDetailsManager = new InMemoryUserDetailsManager(
      //            User.withUsername("tong").password(passwordEncoder().encode("1234565")).authorities("admin").build()
      //    );
      //    return inMemoryUserDetailsManager;
      //}
    }
    

    授权服务器

    @Configuration
    @EnableAuthorizationServer
    public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
      @Autowired
      private PasswordEncoder passwordEncoder;
    
      @Override
      public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
          /**
           *授权码模式
           *http://localhost:8080/oauth/authorize?response_type=code&client_id=client&redirect_uri=http://www.baidu.com&scope=all
           *http://localhost:8080/oauth/authorize?response_type=code&client_id=client
           *
           * implicit: 简化模式
           *http://localhost:8080/oauth/authorize?client_id=client&response_type=token&scope=all&redirect_uri=http://www.baidu.com
           *
           * password模式
           *  http://localhost:8080/oauth/token?username=fox&password=123456&grant_type=password&client_id=client&client_secret=123123&scope=all
           *
           *  客户端模式
           *  http://localhost:8080/oauth/token?grant_type=client_credentials&scope=all&client_id=client&client_secret=123123
           *
           *  刷新令牌
           *  http://localhost:8080/oauth/token?grant_type=refresh_token&client_id=client&client_secret=123123&refresh_token=[refresh_token值]
           */
          clients.inMemory()
                  //配置client_id
                  .withClient("client")
                  //配置client_secret
                  .secret(passwordEncoder.encode("123456"))
                  //配置token有效期
                  .accessTokenValiditySeconds(3600)
                  //配置刷新token的有效期
                  .refreshTokenValiditySeconds(864000)
                  //配置redirect_uri,用于授权成功后跳转
                  .redirectUris("http://www.baidu.com")
                  //配置申请范围
                  .scopes("all")
                  //配置grant_type,表示授权类型
                  .authorizedGrantTypes("authorization_code", "implicit");
      }
    }
    

    资源服务器

    @Configuration
    @EnableResourceServer
    public class ResourceServiceConfig extends ResourceServerConfigurerAdapter {
      @Override
      public void configure(HttpSecurity http) throws Exception {
          http.authorizeRequests()
                  .anyRequest().authenticated()
                  .and()
                  .requestMatchers().antMatchers("/user/**");
      }
    }
    

    测试

    授权码模式

    获取授权码

    http://localhost:8080/oauth/authorize?response_type=code&client_id=client&redirect_uri=http://www.baidu.com&scope=all

    登录
    image.png
    点击获取
    image.png
    获取令牌
    image.png
    image.png
    image.png
    调用方法

  1. 请求方法获取,方法头
    image.png
  2. 请求方法,请求参数
    image.png

    密码模式

    WebSecurityConfig添加Bean注入

     @Bean
     @Override
     public AuthenticationManager authenticationManagerBean() throws Exception {
         return super.authenticationManagerBean();
     }
    

    授权服务器

    @Configuration
    @EnableAuthorizationServer
    public class AuthorizationServerConfig2 extends AuthorizationServerConfigurerAdapter {
     @Autowired
     private UserService userService;
    
     //@Autowired
     //private TokenStore tokenStore;
    
     @Autowired
     private AuthenticationManager authenticationManager;
     @Autowired
     private PasswordEncoder passwordEncoder;
    
     @Override
     public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
         endpoints.authenticationManager(authenticationManager)
                 //.tokenStore(tokenStore)
                 //refresh_token是否重复使用
                 .reuseRefreshTokens(false)
                 //刷新令牌包括对用户信息的检查
                 .userDetailsService(userService)
                 //支持GET,POST请求
                 .allowedTokenEndpointRequestMethods(HttpMethod.GET, HttpMethod.POST);
     }
    
     @Override
     public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
         //允许表单认证
         security.allowFormAuthenticationForClients();
     }
    
     @Override
     public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
         /**
          *授权码模式
          *http://localhost:8080/oauth/authorize?response_type=code&client_id=client&redirect_uri=http://www.baidu.com&scope=all
          *http://localhost:8080/oauth/authorize?response_type=code&client_id=client
          *
          * implicit: 简化模式
          *http://localhost:8080/oauth/authorize?client_id=client&response_type=token&scope=all&redirect_uri=http://www.baidu.com
          *
          * password模式
          *  http://localhost:8080/oauth/token?username=tong&password=123456&grant_type=password&client_id=client&client_secret=123123&scope=all
          *
          *  客户端模式
          *  http://localhost:8080/oauth/token?grant_type=client_credentials&scope=all&client_id=client&client_secret=123123
          *
          *  刷新令牌
          *  http://localhost:8080/oauth/token?grant_type=refresh_token&client_id=client&client_secret=123123&refresh_token=[refresh_token值]
          */
         clients.inMemory()
                 //配置client_id
                 .withClient("client")
                 //配置client-secret
                 .secret(passwordEncoder.encode("123123"))
                 //配置访问token的有效期
                 .accessTokenValiditySeconds(3600)
                 //配置刷新token的有效期
                 .refreshTokenValiditySeconds(864000)
                 //配置redirect_uri,用于授权成功后跳转
                 .redirectUris("http://www.baidu.com")
                 //配置申请的权限范围
                 .scopes("all")
                 /**
                  * 配置grant_type,表示授权类型
                  * authorization_code: 授权码模式
                  * implicit: 简化模式
                  * password: 密码模式
                  * client_credentials: 客户端模式
                  * refresh_token: 更新令牌
                  */
                 .authorizedGrantTypes("authorization_code", "implicit", "password", "client_credentials", "refresh_token");
     }
    }
    

    获取令牌

    http://localhost:8080/oauth/token?username=tong&password=123456&grant_type=password&client_id=client&client_secret=123123&scope=all
    
  3. 浏览器获取token
    通过浏览器测试,需要配置支持get请求和表单认证image.png

  4. postman获取token
    image.png
    image.png

访问资源
image.png

客户端模式

获取令牌

http://localhost:8080/oauth/token?grant_type=client_credentials&scope=all&client_id=client&client_secret=123123

image.png

更新令牌

使用密码模式测试
image.png
刷新

http://localhost:8080/oauth/token?grant_type=refresh_token&client_id=client&client_secret=123123&refresh_token=5ffcf3a7-8215-42a3-8973-7ceba6c220b0

基于redis存储token

引入pom

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-pool2</artifactId>
        </dependency>

application.yml

spring:
  redis:
    host: 139.198.123.65
    port: 6379
    database: 0

reids配置类

@Configuration
public class RedisConfig {
    @Autowired
    private RedisConnectionFactory redisConnectionFactory;
    @Bean
    public TokenStore tokenStore() {
        return new RedisTokenStore(redisConnectionFactory);
    }
}

授权服务器中配置

    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        endpoints.authenticationManager(authenticationManager)
                .tokenStore(tokenStore)
                //refresh_token是否重复使用
                .reuseRefreshTokens(false)
                //刷新令牌包括对用户信息的检查
                .userDetailsService(userService)
                //支持GET,POST请求
                .allowedTokenEndpointRequestMethods(HttpMethod.GET, HttpMethod.POST);
    }

JWT

JSON Web Token(JWT)是一个开放的行业标准(RFC 7519),它定义了一种简介的、自包含的协议格式,用于在通信双方传递json对象,传递的信息经过数字签名可以被验证和信任。JWT可以使用HMAC算法或使用RSA的公钥/私钥对来签名,防止被篡改。
官网: https://jwt.io/
标准: https://tools.ietf.org/html/rfc7519

JWT组成

  1. 头:类型(JWT)以及签名用的算法,然后对头进行base64机密
  2. 荷载:存放有效信息,然后进行base64加密
  3. 签名:header、payload、secret组合加密

image.png
JWT生成是服务端的,是服务端你的私钥,不能流露出去。

JJWT

pom

 <!--JWT依赖-->
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.9.1</version>
</dependency>

创建Token

//创建jwtBuilder
        JwtBuilder jwtBuilder = Jwts.builder()
                //声明标识(jti)
                .setId("888")
                //主体(sub)
                .setSubject("tong")
                //创建日期(iat)
                .setIssuedAt(new Date())
                //过期时间 1分钟
                .setExpiration(new Date(System.currentTimeMillis() + 60 * 1000))
                //传入map
                //.addClaims(map)
                .claim("roles", "admin")
                .claim("log", "xxx.jpg")
                //签名
                .signWith(SignatureAlgorithm.HS256, SECRET_KEY);
        //获取token
        String token = jwtBuilder.compact();

token解析

        //解析token获取载荷中的声明对象
        Claims claims = Jwts.parser()
                .setSigningKey(SECRET_KEY)
                .parseClaimsJws(token)
                .getBody();

Spring Security Oauth2 整合 JWT

引入pom

<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-jwt</artifactId>
    <version>1.0.9.RELEASE</version>
</dependency>

添加JwtTokenStoreConfig

@Configuration
public class JwtTokenStoreConfig {
    @Bean
    public TokenStore jwtTokenStore() {
        return new JwtTokenStore(jwtAccessTokenConverter());
    }

    @Bean
    public JwtAccessTokenConverter jwtAccessTokenConverter() {
        JwtAccessTokenConverter jwtAccessTokenConverter = new JwtAccessTokenConverter();
        jwtAccessTokenConverter.setSigningKey("123123");
        return jwtAccessTokenConverter;
    }
}

在授权服务器指定jwt

    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        //配置JWT的内容增强器
        TokenEnhancerChain enhancerChain = new TokenEnhancerChain();
        List<TokenEnhancer> delegates = new ArrayList<>();
        delegates.add(jwtTokenEnhancer);
        delegates.add(jwtAccessTokenConverter);
        enhancerChain.setTokenEnhancers(delegates);

        //密码模式配置
        endpoints.authenticationManager(authenticationManager)
                .tokenStore(tokenStore)
                .accessTokenConverter(jwtAccessTokenConverter)
                .tokenEnhancer(enhancerChain)
                //refresh_token是否重复使用
                .reuseRefreshTokens(false)
                //刷新令牌包括对用户信息的检查
                .userDetailsService(userService)
                //支持GET,POST请求
                .allowedTokenEndpointRequestMethods(HttpMethod.GET, HttpMethod.POST);
    }

扩展JWT存储内容

@Component
public class JwtTokenEnhancer implements TokenEnhancer {

    @Override
    public OAuth2AccessToken enhance(OAuth2AccessToken accessToken,
                                     OAuth2Authentication authentication) {
        Map<String, Object> info = new HashMap<>();
        info.put("enhance", "enhance info");
        ((DefaultOAuth2AccessToken) accessToken).setAdditionalInformation(info);
        return accessToken;
    }
}

解析JWT

pom

<!--JWT依赖-->
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.9.1</version>
</dependency>

查看

    @RequestMapping("/getCurrentUser")
    public Object getCurrentUser(Authentication authentication, HttpServletRequest request) {
        String header = request.getHeader("Authorization");
        String token = null;
        if (header != null) {
            token = header.substring(header.indexOf("bearer") + 7);
        } else {
            token = request.getParameter("access_token");
        }
        return Jwts.parser()
                .setSigningKey("123123".getBytes(StandardCharsets.UTF_8))
                .parseClaimsJws(token)
                .getBody();
    }