spring-cloud-oauth2 认证授权


什么是OAuth2?

  OAuth2是一个关于授权的开放标准,核心思路是通过各类认证手段(具体什么手段OAuth2不关心)认证用户身份,并颁发token(令牌),使得第三方应用可以使用该令牌在限定时间、限定范围访问指定资源。主要涉及的RFC规范有RFC6749(整体授权框架),RFC6750(令牌使用),RFC6819(威胁模型)这几个,一般我们需要了解的就是RFC6749。获取令牌的方式主要有四种,分别是授权码模式简单模式密码模式客户端模式。这里要先明确几个OAuth2中的几个重要概念:

  • resource_owner : 拥有被访问资源的用户
  • user-agent: 一般来说就是浏览器
  • client : 第三方应用
  • Authorization server : 认证服务器,用来进行用户认证并颁发token
  • Resource server: 资源服务器,拥有被访问资源的服务器,需要通过token来确定是否有权限访问

  我们在浏览器端或者APP端做登录的时候时常会遇到 QQ登录、微信登陆、微博登录 等等。这一类称之为第三方登录。在APP端 往往会采用OAuth2。以QQ登录为准,通常是点击了QQ登录,首先跳转到QQ登录授权页面进行扫码授权。然后跳回原来网页设定好的一个回调地址。这其实就完成了OAuth的整个授权流程。OAuth在"客户端"与"服务提供商"之间,设置了一个授权层(authorization layer)。"客户端"不能直接登录"服务提供商",只能登录授权层,以此将用户与客户端区分开来。"客户端"登录授权层所用的令牌(token),与用户的密码不同。用户可以在登录的时候,指定授权层令牌的权限范围和有效期。"客户端"登录授权层以后,"服务提供商"根据令牌的权限范围和有效期,向"客户端"开放用户储存的资料。

OAuth2 运行流程:

  OAuth 2.0的运行流程如下图,摘自RFC6749。

  1. 用户打开客户端以后,客户端要求用户给予授权。( QQ登录跳转到授权页面)
  2. 用户同意给予客户端授权。  (用户扫码确定授权)
  3. 客户端使用上一步获得的授权,向认证服务器申请令牌。(跳转到回调地址,且携带一个 code )
  4. 认证服务器对客户端进行认证以后,确认无误,同意发放令牌。  (通过上一步得到的code 进行授权码认证)
  5. 客户端使用令牌,向资源服务器申请获取资源。 (用换取到的 access_token 进行访问资源)
  6. 资源服务器确认令牌无误,同意向客户端开放资源。 (token 认证通过 返回数据)。

授权方式:

  客户端必须得到用户的授权(authorization grant),才能获得令牌(access token)。OAuth 2.0定义了四种授权方式。

  • 授权码模式(authorization code)
  • 简化模式(implicit)
  • 密码模式(resource owner password credentials)
  • 客户端模式(client credentials)

  本文主要介绍 授权码模式 跟 密码模式。

授权认证服务实现:

  搭建认证服务 Authorization server:

1.导入依赖(包括后续要用到的一些依赖),这里 springboot 2.0.1 、springCloud 版本为 Finchley.SR3:

<dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-oauth2</artifactId>
        </dependency>
        <!-- https://mvnrepository.com/artifact/org.springframework.cloud/spring-cloud-starter-security -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-security</artifactId>
            <version>2.2.1.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
        </dependency>
        <dependency>
            <groupId>commons-lang</groupId>
            <artifactId>commons-lang</artifactId>
            <version>2.6</version>
        </dependency>
        <dependency>
            <groupId>commons-collections</groupId>
            <artifactId>commons-collections</artifactId>
            <version>3.2.1</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.session</groupId>
            <artifactId>spring-session-data-redis</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.security.oauth</groupId>
            <artifactId>spring-security-oauth2</artifactId>
            <version>2.3.3.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
            <version>0.7.0</version>
        </dependency>
    </dependencies>

2. 认证服务器配置,要实现认证服务器其实很简单,只要打上 @EnableAuthorizationServer 注解,然后继承 AuthorizationServerConfigurerAdapter 进行一些简单的配置即可。

@Configuration @EnableAuthorizationServer public class WuzzAuthorizationServerConfig extends AuthorizationServerConfigurerAdapter { //http://localhost:8766/oauth/authorize?client_id=wuzzClientId&response_type=code&redirect_uri=http://www.baidu.com&scope=all
 @Override public void configure(ClientDetailsServiceConfigurer clients) throws Exception { clients.inMemory().withClient("wuzzClientId")//客户端得ID,比如我们在QQ互联中心申请得。可以写多个。配置 循环
                .secret(passwordEncoder().encode("wuzzSecret")) // 客户端密钥,需要进行加密
                .accessTokenValiditySeconds(7200)// token 有效时常 0 永久有效
                .authorizedGrantTypes("password", "implicit", "refresh_token", "authorization_code")// 支持得授权类型
                .redirectUris("http://www.baidu.com")//回调地址
                .scopes("all", "read", "write");//拥有的 scope 可选
 } @Override public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception { endpoints.userDetailsService(userDetailsService()) // 用户信息得服务,一版是都数据库
                .authenticationManager(authenticationManager())// 认证管理器。
 .allowedTokenEndpointRequestMethods(HttpMethod.GET, HttpMethod.POST); } @Override public void configure(AuthorizationServerSecurityConfigurer security) throws Exception { security.allowFormAuthenticationForClients()//允许表单登录
                .checkTokenAccess("permitAll()"); //开启/oauth/check_token验证端口认证权限访问
 } @Bean // 注入认证管理器
    public AuthenticationManager authenticationManager() { AuthenticationManager authenticationManager = new AuthenticationManager() { @Override public Authentication authenticate(Authentication authentication) throws AuthenticationException { return daoAuthenticationProvider().authenticate(authentication); } }; return authenticationManager; } @Bean//注入认证器
    public AuthenticationProvider daoAuthenticationProvider() { DaoAuthenticationProvider daoAuthenticationProvider = new DaoAuthenticationProvider(); daoAuthenticationProvider.setUserDetailsService(userDetailsService()); daoAuthenticationProvider.setHideUserNotFoundExceptions(false); daoAuthenticationProvider.setPasswordEncoder(passwordEncoder()); return daoAuthenticationProvider; } @Bean//注入 用户信息服务
    public UserDetailsService userDetailsService() { return new MyUserDetailService(); } @Bean//注入密码加密
    public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } }

3.由于 OAuth2 依赖于 Security 得配置,所以我们这里还需要配置一下  Security :

@Configuration public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests().antMatchers("/**").fullyAuthenticated().and().httpBasic(); } }

4.自定义的用户信息服务类,由于Oauth 的用户需要有个  ROLE_USER 角色 才可以访问,所以这里写死。

public class MyUserDetailService implements UserDetailsService { private Logger logger = LoggerFactory.getLogger(getClass()); @Autowired private PasswordEncoder passwordEncoder; @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { logger.info("表单登录用户名:" + username); // 根据用户名查找用户信息 //根据查找到的用户信息判断用户是否被冻结
        String password = passwordEncoder.encode("123456"); logger.info("数据库密码是:" + password); return new User(username, password, true, true, true, true, AuthorityUtils.commaSeparatedStringToAuthorityList("admin,ROLE_USER")); } }

5.启动主类即可进行访问。

  授权码模式:

  授权码需要访问接口 : http://localhost:8766/oauth/authorize?client_id=wuzzClientId&response_type=code&redirect_uri=http://www.baidu.com&scope=all

  其中 client_id 为认证服务器为每个对接的第三方提供的唯一ID。response_type 返回类型,写死为 code 。redirect_uri 回调地址。

  访问该地址,如果用户当前未登录将会跳转到用户登录页面进行登录。然后将会跳转到下面这个页面。询问用户是否为 wuzzClientId这个应用授权。

   点击授权,将会跳转到回调地址页,由于没有备案域名,这里直接跳到百度:

   可以看到这里后面携带了 一个 code 参数,这个参数就是认证服务器为第三方提供的授权码。然后再用这个授权码去换取 access_token。我这里就用 postman 进行测试:

  换取 access_token得地址为 /oauth/token,首先需要填入认证服务器颁发的 clientId、client-secret

  然后填写参数 ,发送请求。注意这里前三个参数是必填的。

   可以看到这样就可以成功的获取到 access_token 了。然后第三方用户就可以通过这个 token 去资源服务器上获取授权的用户信息了。后续会提到这个token 怎么用。

  密码模式:

  相比授权码授权方式来说,密码模式相对简单,我们只需要修改授权类型,增加 用户名、密码 字段:

 

  细心的小伙伴可能会发现,我这里用的是同一个用户  admin 去获取token,获取到的 access_token、refresh_token 都是一样的 ,唯独 expires_in(过期时间)逐渐减少。这是Oauth 提供的机制。在这个  expires_in 时间内 access_token都是有效的。当然,refresh_token  用于刷新 access_token,避免了用户的频繁认证,刷新token请求如下:

资源服务器 Resource server:

1.配置资源服务器就更简单了,新建一个 Springboot 标准工程,导入与认证服务器一样的依赖,然后定义一个类,打上 @EnableResourceServer 注解,实现 ResourceServerConfigurerAdapter 进行简单配置:

@Configuration @EnableResourceServer public class WuzzResourceServerConfig extends ResourceServerConfigurerAdapter { @Override public void configure(HttpSecurity http) throws Exception { //配置受保护的资源
        http.authorizeRequests().antMatchers("/api/order/**").authenticated(); } }

2.配置文件:

server.port = 8765 #check_token url security.oauth2.resource.token-info-uri= http://localhost:8766/oauth/check_token
security.oauth2.resource.prefer-token-info= true # authorize url security.oauth2.client.access-token-uri=http://localhost:8766/oauth/authorize
#用户认证地址 check_token security.oauth2.client.user-authorization-uri=http://localhost:8766/oauth/check_token
security.oauth2.client.client-id=wuzzClientId security.oauth2.client.client-secret=wuzzSecret

3.提供一个测试接口

@RestController @RequestMapping("/api/order") public class OrderController {
@RequestMapping(
"addOrder") public String addOrder(){ return "addOrder"; } }

4.启动服务,当然,你想直接访问这个接口显然是不行的

 

   这个时候我们带上之前获取到的  token ,过期的话重新获取一个:

   这样就实现了资源服务器与认证服务器的打通。

Token 存储:

  OAuth2存储token值的方式由多种,所有的实现方式都是实现了TokenStore接口

  1. InMemoryTokenStore:token存储在本机的内存之中
  2. JdbcTokenStore:token存储在数据库之中
  3. JwtTokenStore:token不会存储到任何介质中
  4. RedisTokenStore:token存储在Redis数据库之中

   这里使用 Redis 进行存储演示:

1.配置 redis :

# Redis服务地址 spring.redis.host=192.168.1.101 # Redis服务端口 spring.redis.port=6379 # Redis 连接密码 spring.redis.password=wuzhenzhao

2.新增Redis连接工厂:

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

3.配置,再 WuzzAuthorizationServerConfig 中新增如下配置。

// 自定义token存储类型
@Autowired private TokenStore tokenStore;  @Override public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {   endpoints.userDetailsService(userDetailsService()) // 用户信息得服务,一版是都数据库
    .authenticationManager(authenticationManager())// 认证管理器。
    .allowedTokenEndpointRequestMethods(HttpMethod.GET, HttpMethod.POST)     .tokenStore(tokenStore); }

4.启动服务并且通过密码授权获取 access_token.然后查看Redis 上的数据变化:

   可以发现 token 已经被存储到了 redis上面,然后我们把认证服务器重启,然后拿着哲哥 access_token 去访问资源服务器,发现依旧可以访问得到。Redis token 配置成功。

JWT 整合:

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

  JWT 的几个特点

  • JWT 默认是不加密,但也是可以加密的。生成原始 Token 以后,可以用密钥再加密一次。
  • JWT 不加密的情况下,不能将秘密数据写入 JWT。
  • JWT 不仅可以用于认证,也可以用于交换信息。有效使用 JWT,可以降低服务器查询数据库的次数。
  • JWT 的最大缺点是,由于服务器不保存 session 状态,因此无法在使用过程中废止某个 token,或者更改 token 的权限。也就是说,一旦 JWT 签发了,在到期之前就会始终有效,除非服务器部署额外的逻辑。
  • JWT 本身包含了认证信息,一旦泄露,任何人都可以获得该令牌的所有权限。为了减少盗用,JWT 的有效期应该设置得比较短。对于一些比较重要的权限,使用时应该再次对用户进行认证。
  • 为了减少盗用,JWT 不应该使用 HTTP 协议明码传输,要使用 HTTPS 协议传输。

  它是一个很长的字符串,中间用点(.)分隔成三个部分。注意,JWT 内部是没有换行的,这里只是为了便于展示,将它写成了几行。JWT 的三个部分依次如下。

  • Header(头部)
  • Payload(负载)
  • Signature(签名)

   如下就是一个 JWT  :

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJ1c2VyX25hbWUiOiJhZG1pbiIsInNjb3BlIjpbImFsbCJdLCJjb21wYW55IjoiYWxpYmFiYSIsImV4cCI6MTU5NDEwMTA2OSwiYXV0aG9yaXRpZXMiOlsiYWRtaW4iLCJST0xFX1VTRVIiXSwianRpIjoiMzQ4MmM4YmEtYjdmYy00NDIxLWIwZmItYzVhYjhlOGUzYzY2IiwiY2xpZW50X2lkIjoid3V6ekNsaWVudElkIn0.
-DxGM5URWqHOZE5mmH4CgJI_bX-e9THA9WeQeT7Z5qU

  像这个 token 我们可以借助第三方进行解码 : https://www.jsonwebtoken.io/ .通过该网址就可以看到包含的所有信息。

1.注入 Jwt 相关类:

@Configuration public class TokenStoreConfig { @Autowired private RedisConnectionFactory redisConnectionFactory; @Bean @ConditionalOnProperty(prefix = "wuzz", name = "storeType", havingValue = "redis") public TokenStore redisTokenStore() { return new RedisTokenStore(redisConnectionFactory); } @Configuration @ConditionalOnProperty(prefix = "wuzz", name = "storeType", havingValue = "jwt", matchIfMissing = true) public static class JwtTokenConfig { //自包含、可拓展、密签 //https://www.jsonwebtoken.io/ 解码 //{ // "exp": 1593785308, // "user_name": "admin", // "authorities": [ // "admin", // "ROLE_USER" // ], // "jti": "e2e5e811-b235-49b8-8678-5bf22e265415", // "client_id": "wuzzClientId", // "scope": [ // "all" // ] //}
        @Bean// 注入 jwt 存储 token
        public TokenStore jwtTokenStore() { return new JwtTokenStore(jwtAccessTokenConverter()); } @Bean// 注入转换器
        public JwtAccessTokenConverter jwtAccessTokenConverter() { JwtAccessTokenConverter accessTokenConverter = new JwtAccessTokenConverter(); accessTokenConverter.setSigningKey("wuzz");//
            return accessTokenConverter; } @Bean//添加 token 包含信息
        @ConditionalOnMissingBean(name = "jwtTokenEnhancer") public TokenEnhancer jwtTokenEnhancer() { return new WuzzJwtTokenEnhancer(); } } }

2.配置文件新增:

wuzz.storeType=jwt

3.在 WuzzAuthorizationServerConfig 中配置:

// 自定义token存储类型
 @Autowired private TokenStore tokenStore; // jwt token
    @Autowired(required = false) private JwtAccessTokenConverter jwtAccessTokenConverter; //jwt token 附加信息
    @Autowired(required = false) private TokenEnhancer jwtTokenEnhancer; @Override public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception { endpoints.userDetailsService(userDetailsService()) // 用户信息得服务,一版是都数据库
                .authenticationManager(authenticationManager())// 认证管理器。
 .allowedTokenEndpointRequestMethods(HttpMethod.GET, HttpMethod.POST) .tokenStore(tokenStore); if (jwtAccessTokenConverter != null && jwtTokenEnhancer != null) { TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain(); List<TokenEnhancer> enhancers = new ArrayList<>(); enhancers.add(jwtTokenEnhancer); enhancers.add(jwtAccessTokenConverter); tokenEnhancerChain.setTokenEnhancers(enhancers); endpoints.tokenEnhancer(tokenEnhancerChain) .accessTokenConverter(jwtAccessTokenConverter); } }

4. 自定义 token 附加信息实现:

public class WuzzJwtTokenEnhancer implements TokenEnhancer { @Override public OAuth2AccessToken enhance(OAuth2AccessToken oAuth2AccessToken, OAuth2Authentication oAuth2Authentication) { Map<String, Object> info = new HashMap<String, Object>(); info.put("company", "alibaba"); ((DefaultOAuth2AccessToken) oAuth2AccessToken).setAdditionalInformation(info); return oAuth2AccessToken; } }

5.启动认证服务器用密码认证方式获取一下 access_token ,发现token已经发生了变化,而且我们在token里增加的属性也显示出来了:

  我们可以通过在资源服务器中写一个解析这个 token的方法:

@RequestMapping(value = "/me", method = {RequestMethod.GET}) public Object me(Authentication user, HttpServletRequest request) throws UnsupportedEncodingException { String header = request.getHeader("Authorization"); String token = StringUtils.substringAfter(header, "Bearer "); Claims claims = Jwts.parser().setSigningKey("wuzz".getBytes("UTF-8")).parseClaimsJws(token).getBody(); String company = (String) claims.get("company"); System.out.println(company); return user; }

   然后请求该接口可以获取到相关的信息。

 

整合 JdbcClientDetailsService :

  在上文中我们讲 client的信息都是写死在配置里面,显然在生产环境下是不合理的,OAuth2 提供了相应的配置。

1.导入依赖:

<dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jdbc</artifactId>
        </dependency>

2.修改配置:

@Autowired private DataSource dataSource; @Override public void configure(ClientDetailsServiceConfigurer clients) throws Exception { // clients.inMemory().withClient("wuzzClientId")//客户端得ID,比如我们在QQ互联中心申请得。可以写多个。配置 循环 // .secret(passwordEncoder().encode("wuzzSecret")) // 客户端密钥,需要进行加密 // .accessTokenValiditySeconds(7200)// token 有效时常 0 永久有效 // .authorizedGrantTypes("password", "implicit", "refresh_token", "authorization_code")// 支持得授权类型 // .redirectUris("http://www.baidu.com")//回调地址 // .scopes("all", "read", "write");//拥有的 scope 可选
  clients.withClientDetails(new JdbcClientDetailsService(dataSource)); }

3.新增数据库配置:

#解决springboot2.0 后内存数据库H2与actuator不能同时使用报datasource循环依赖 spring.cloud.refresh.refreshable=none spring.datasource.driver-class-name=com.mysql.jdbc.Driver spring.datasource.url=jdbc:mysql://192.168.1.101:3306/study?useUnicode=true&characterEncoding=utf-8
spring.datasource.username=root spring.datasource.password=123456

4.数据库新增对应表,并添加一条数据:

-- ---------------------------- -- Table structure for oauth_client_details -- ----------------------------
DROP TABLE IF EXISTS `oauth_client_details`; CREATE TABLE `oauth_client_details` ( `client_id` varchar(48) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL, `resource_ids` varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, `client_secret` varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, `scope` varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, `authorized_grant_types` varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, `web_server_redirect_uri` varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, `authorities` varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, `access_token_validity` int(11) NULL DEFAULT NULL, `refresh_token_validity` int(11) NULL DEFAULT NULL, `additional_information` varchar(4096) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, `autoapprove` varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, PRIMARY KEY (`client_id`) USING BTREE ) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Compact; -- ---------------------------- -- Records of oauth_client_details -- ----------------------------
INSERT INTO `oauth_client_details` VALUES ('wuzzClientId', NULL, '$2a$10$L2juyPBc606/9xkmFWu5S.5PBjfz6IXxtUnl8Bk9B2s9Bbn1TPO.2', 'all', 'password', 'http://www.baidu.com', NULL, NULL, NULL, NULL, NULL);

 5.重启服务,按照原来的方式通过用户名密码进行授权,也是可以实现的。


免责声明!

本站转载的文章为个人学习借鉴使用,本站对版权不负任何法律责任。如果侵犯了您的隐私权益,请联系本站邮箱yoyou2525@163.com删除。



 
粤ICP备18138465号  © 2018-2025 CODEPRJ.COM