文章较长,建议慢慢看,建议收藏

本文简述

OAuth(敞开授权)是一个敞开标准,答运用户授权第三方运用拜访他们存储在别的的服务提供者上的信息,而不需求将用户名和暗码提供给第三方运用或共享他们数据的一切内容

OAuth 2 有四种授权形式,分别是授权码形式(authorization code)、简化形式(implicit)、暗码形式(resource owner password credentials)、客户端形式(client credentials)

本文咱们将运用授权码形式和暗码形式两种办法来完结用户认证和授权办理,共两篇文章

OAuth基本运用

首要咱们最了解的便是几乎每个人都用过的,比方用微信登录、用 QQ 登录、用微博登录、用 Google 账号登录、用 github 授权登录等等,这些都是典型的 OAuth2 运用场景。假定咱们做了一个自己的服务渠道,假如不运用 OAuth2 登录办法,那么咱们需求用户先完结注册,然后用注册号的账号暗码或许用手机验证码登录。而运用了 OAuth2 之后,相信很多人运用过、甚至开发过公众号网页服务、小程序,当咱们进入网页、小程序界面,第一次运用就无需注册,直接运用微信授权登录即可,大大提高了运用效率。因为每个人都有微信号,有了微信就能够马上运用第三方服务,这体会不要太好了。而关于咱们的服务来说,咱们也不需求存储用户的暗码,只需存储认证渠道回来的唯一ID 和用户信息即可

以上是运用了 OAuth2 的授权码形式,利用第三方的权威渠道完结用户身份的认证。当然了,假如你的公司内部有很多个服务,能够专门提取出一个认证中心,这个认证中心就充任上面所说的权威认证渠道的人物,一切的服务都要到这个认证中心做认证。

这样一说,发现没,这其实便是个单点登录的功用。这便是别的一种运用场景,关于多服务的渠道,能够运用 OAuth2 完结服务的单点登录,只做一次登录,就能够在多个服务中自在穿行,当然仅限于授权范围内的服务和接口。

完结统一认证服务

本篇先介绍暗码形式(resource owner password credentials),下一篇介绍授权码形式

微服务横行的今日,一般的公司都会运用微服务进行开发,微服务很好的对服务之间解耦,一起也在某些方面增加了体系的复杂度,比方说用户认证。假定咱们这儿完结了一个电商渠道,用户看到的便是一个 APP 或许一个 web 站点,实际上背后是由多个独立的服务构成的,比方用户服务、订单服务、产品服务等。用户只需第一次输入用户名、暗码完结登录后,一段时刻内,都能够恣意拜访各个页面,比方产品列表页面、我的订单页面、我的重视等页面

咱们能够想象一下,自然能够想到,在恳求各个服务、各个接口的时分,必定携带着什么凭证,然后各个服务才知道恳求接口的用户是哪个,不然肯定有问题,那其实这儿面的凭证简略来说便是一个 Token,标识用户身份的 Token

举个例子来对这个流程进行阐明:

认证中心:oauth2-auth-server,OAuth2 首要完结端,Token 的生成、改写、验证都在认证中心完结。

订单服务:oauth2-client-order-server,微服务之一,接收到恳求后会到认证中心验证。

用户服务:oauth2-client-user-server,微服务之二,接收到恳求后会到认证中心验证。

客户端:例如 APP 端、web 端 等终端

大致的流程是:客户端去认证中心申请拜访凭证token,然后认证中心关于客户端恳求来的帐号暗码进行验证,假如验证经过,则颁布token,回来给客户端,客户端拿着 token 去各个微服务恳求数据接口,一般这个 token 是放到 header 中的。当微服务接到恳求后,先要拿着 token 去认证服务端查看 token 的合法性,假如合法,再依据用户所属的人物及具有的权限动态的回来数据

创立并装备认证服务

认证服务的功用,首要是验证帐号暗码,存储token,验证token,颁布token,改写token,这些都是认证服务需求做的

首要引进maven装备

<dependency>
 <groupId>org.springframework.boot</groupId>
 <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
 <groupId>org.springframework.cloud</groupId>
 <artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>
<dependency>
 <groupId>org.springframework.boot</groupId>
 <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
 <groupId>org.springframework.boot</groupId>
 <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

spring-cloud-starter-oauth2包含了spring-cloud-starter-security,所以不必再独自引进了。之所以引进 redis 包,是因为下面会介绍一种用 redis 存储 token 的办法

装备application.yml

spring:
application:
  name: auth-server
redis:
  database: 2
  host: localhost
  port: 32768
  password: 1qaz@WSX
  jedis:
    pool:
      max-active: 8
      max-idle: 8
      min-idle: 0
  timeout: 100ms
server:
port: 6001
management:
endpoint:
  health:
    enabled: true

springsecurity基础装备

@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    @Bean
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }
    @Bean
    public AuthenticationManager authenticationManager() throws Exception {
        return super.authenticationManager();
    }
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .antMatchers("/**").permitAll();
    }
}

运用@EnableWebSecurity注解修饰,并承继自WebSecurityConfigurerAdapter

这个类的要点便是声明PasswordEncoderAuthenticationManager两个 Bean。稍后会用到。其间BCryptPasswordEncoder是一个暗码加密工具类,它能够完结不可逆的加密,AuthenticationManager是为了完结 OAuth2 的 password 形式必需求指定的授权办理 Bean

完结UserDetailsService

了解security的应该知道,UserDetailsService是用来对用户身份进行验证的一种办法,首要的办法是loadUserByUsername,它要接收一个字符串参数,也便是传过来的用户名,回来一个UserDetails目标

@Slf4j
@Component
public class MyUserDetailsService implements UserDetailsService {
    @Autowired
    private PasswordEncoder passwordEncoder;
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        log.info("usernameis:" + username);
        // 查询数据库操作
        if(!username.equals("admin")){
            throw new UsernameNotFoundException("the user is not found");
        }else{
            // 用户人物也应在数据库中获取
            String role = "ROLE_ADMIN";
            List<SimpleGrantedAuthority> authorities = new ArrayList<>();
            authorities.add(new SimpleGrantedAuthority(role));
            // 线上环境应该经过用户名查询数据库获取加密后的暗码
            String password = passwordEncoder.encode("123456");
            //最后验证经过后,把用户的账号暗码和权限等信息封装到UserDetails中回来给上级
            return new org.springframework.security.core.userdetails.User(username,password, authorities);
        }
    }
}

这儿为了做演示,把用户名、暗码和所属人物都写在代码里了,正式环境中,这儿应该是从数据库或许其他地方依据用户名将加密后的暗码及所属人物查出来的。账号 admin ,暗码 123456,稍后交换 token 的时分会用到。并且给这个用户设置 “ROLE_ADMIN” 人物。

OAuth装备文件

创立一个装备文件集成自AuthorizationServerConfigurerAdapter,完结其间的condfigure办法

@Configuration
public class OAuth2Config extends AuthorizationServerConfigurerAdapter {
    @Resource
    private PasswordEncoder passwordEncoder;
    @Resource
    private AuthenticationManager authenticationManager;
    @Resource
    private TokenStore redisTokenStory;
    @Resource
    private UserDetailsService myUserDetailService;
    /**
     * 用来装备客户端概况服务(ClientDetailsService),
     客户端概况信息在这儿进行初始化,你能够把客户端概况信息写死在这儿或许是经过数据库来存储调取概况信息
     * @param clients
     * @throws Exception
     */
    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        clients.inMemory()
                .withClient("order-client")
                .secret(passwordEncoder.encode("order-secret-8888"))
                .authorizedGrantTypes("refresh_token","authorization_code", "password")
                .accessTokenValiditySeconds(3600)
                .scopes("all")
                .and()
                .withClient("user-client")
                .secret(passwordEncoder.encode("user-secret-8888"))
                .authorizedGrantTypes("refresh_token", "authorization_code", "password")
                .accessTokenValiditySeconds(3600)
                .scopes("all");
    }
    /**
     * 用来装备令牌的拜访端点和令牌服务
     * @param endpoints
     * @throws Exception
     */
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        endpoints.authenticationManager(authenticationManager)
                .userDetailsService(myUserDetailService)
                .tokenStore(redisTokenStory);
    }
    /**
     * 用来装备令牌端点的安全约束
     * @param security
     * @throws Exception
     */
    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
        security.allowFormAuthenticationForClients();
        security.checkTokenAccess("isAuthenticated()");
        security.tokenKeyAccess("isAuthenticated()");
    }
}

有三个 configure 办法的重写。

AuthorizationServerEndpointsConfigurer参数的重写

authenticationManage() 调用此办法才干支撑 password 形式。

userDetailsService() 设置用户验证服务。

tokenStore() 指定 token 的存储办法。

redisTokenStore Bean 的界说如下:

@Configuration
public class RedisTokenStoreConfig {
    @Resource
    public RedisConnectionFactory redisConnectionFactory;
    @Bean
    public TokenStore redisTokenStore(){
        return new RedisTokenStore(redisConnectionFactory);
    }
}

ClientDetailsServiceConfigurer参数的重写,在这儿界说各个端的约束条件。包含

ClientId、Client-Secret:这两个参数对应恳求端界说的 cleint-id 和 client-secret

authorizedGrantTypes 能够包含如下几种设置中的一种或多种:

  • authorization_code:授权码类型。
  • implicit:隐式授权类型。
  • password:资源一切者(即用户)暗码类型。
  • client_credentials:客户端凭证(客户端ID以及Key)类型。
  • refresh_token:经过以上授权取得的改写令牌来获取新的令牌。

accessTokenValiditySeconds:token 的有用期

scopes:用来限制客户端拜访的权限,在交换的 token 的时分会带上 scope 参数,只有在 scopes 界说内的,才能够正常交换 token。

上面代码中是运用 inMemory 办法存储的,将装备保存到内存中,相当于硬编码了。正式环境下的做法是持久化到数据库中,比方 mysql 中

还有一个重写的办法public void configure(AuthorizationServerSecurityConfigurer security),这个办法限制客户端拜访认证接口的权限


security.allowFormAuthenticationForClients();
security.checkTokenAccess("isAuthenticated()");
security.tokenKeyAccess("isAuthenticated()");

第一行代码是答应客户端拜访 OAuth2 授权接口,不然恳求 token 会回来 401。

第二行和第三行分别是答应已授权用户拜访 checkToken 接口和获取 token 接口。

完结之后,发动项目,假如你用的是 IDEA 会在下方的 Mapping 窗口中看到 oauth2 相关的 RESTful 接口

首要有如下几个:

POST /oauth/authorize 授权码形式认证授权接口
GET/POST /oauth/token 获取 token 的接口
POST /oauth/check_token 查看 token 合法性接口

创立用户客户端项目

application.yml装备文件

spring:
application:
  name: client-user
redis:
  database: 2
  host: localhost
  port: 32768
  password: 1qaz@WSX
  jedis:
    pool:
      max-active: 8
      max-idle: 8
      min-idle: 0
  timeout: 100ms
server:
port: 6101
servlet:
  context-path: /client-user
security:
oauth2:
  client:
    client-id: user-client
    client-secret: user-secret-8888
    user-authorization-uri: http://localhost:6001/oauth/authorize
    access-token-uri: http://localhost:6001/oauth/token
  resource:
    id: user-client
    user-info-uri: user-info
  authorization:
    check-token-access: http://localhost:6001/oauth/check_token

上面是常规装备信息以及 redis 装备,要点是下面的 security 的装备,这儿的装备稍有不注意就会出现 401 或许其他问题。

client-id、client-secret 要和认证服务中的装备共同,假如是运用 inMemory 还是 jdbc 办法。

user-authorization-uri 是授权码认证办法需求的,下一篇文章再说。

access-token-uri 是暗码形式需求用到的获取 token 的接口。

authorization.check-token-access 也是要害信息,当此服务端接收到来自客户端端的恳求后,需求拿着恳求中的 token 到认证服务端做 token 验证,便是恳求的这个接口

资源装备文件

在 OAuth2 的概念里,一切的接口都被称为资源,接口的权限也便是资源的权限,所以 Spring Security OAuth2 中提供了关于资源的注解@EnableResourceServer,和@EnableWebSecurity的作用类似

@Configuration
@EnableResourceServer
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
   @Value("${security.oauth2.client.client-id}")
   private String clientId;
   @Value("${security.oauth2.client.client-secret}")
   private String secret;
   @Value("${security.oauth2.authorization.check-token-access}")
   private String checkTokenEndpointUrl;
   @Autowired
   private RedisConnectionFactory redisConnectionFactory;
   @Bean
   public TokenStore redisTokenStore (){
       return new RedisTokenStore(redisConnectionFactory);
  }
   @Bean
   public RemoteTokenServices tokenService() {
       RemoteTokenServices tokenService = new RemoteTokenServices();
       tokenService.setClientId(clientId);
       tokenService.setClientSecret(secret);
       tokenService.setCheckTokenEndpointUrl(checkTokenEndpointUrl);
       return tokenService;
  }
   @Override
   public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
       resources.tokenServices(tokenService());
  }
}

因为运用的是 redis 作为 token 的存储,所以需求特别装备一下叫做 tokenService 的 Bean,经过这个 Bean 才干完结 token 的验证

最后添加一个测验接口

@Slf4j
@RestController
public class UserController {
   @GetMapping(value = "get")
   //@PreAuthorize("hasAuthority('ROLE_ADMIN')")
   @PreAuthorize("hasAnyRole('ROLE_ADMIN')")
   public Object get(Authentication authentication){
       //Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
       authentication.getCredentials();
       OAuth2AuthenticationDetails details = (OAuth2AuthenticationDetails)authentication.getDetails();
       String token = details.getTokenValue();
       return token;
  }
}

一个 RESTful 办法,只有当拜访用户具有 ROLE_ADMIN 权限时才干拜访,不然回来 401 未授权
经过 Authentication 参数或许SecurityContextHolder.getContext().getAuthentication()能够拿到授权信息进行查看

测验认证功用

1、发动认证服务端,发动端口为 6001

2、发动用户服务客户端,发动端口为6101

3、恳求认证服务端获取 token

POST http://localhost:6001/oauth/token?grant_type=password&username=admin&password=123456&scope=all
Accept: */*
Cache-Control: no-cache
Authorization: Basic dXNlci1jbGllbnQ6dXNlci1zZWNyZXQtODg4OA==

假定咱们在一个 web 端运用,grant_type 是 password,标明这是运用 OAuth2 的暗码形式

username=admin 和 password=123456 就相当于在 web 端登录界面输入的用户名和暗码,咱们在认证服务端装备中固定了用户名是 admin 、暗码是 123456,而线上环境中则应该经过查询数据库获取

scope=all 是权限有关的,在认证服务的 OAuthConfig 中指定了 scope 为 all

Authorization 要加在恳求头中,格局为 Basic 空格 base64(clientId:clientSecret),这个微服务客户端的 client-id 是 user-client,client-secret 是 user-secret-8888,将这两个值经过冒号连接,并运用 base64 编码(user-client:user-secret-8888)之后的值为 dXNlci1jbGllbnQ6dXNlci1zZWNyZXQtODg4OA==,能够经过 www.sojson.com/base64.html 在线编码获取

运转恳求后,假如参数都正确的话,获取到的回来内容如下,是一段 json 格局

{
 "access_token": "9f958300-5005-46ea-9061-323c9e6c7a4d",
 "token_type": "bearer",
 "refresh_token": "0f5871f5-98f1-405e-848e-80f641bab72e",
 "expires_in": 3599,
 "scope": "all"
}

access_token : 便是之后恳求需求带上的 token,也是本次恳求的首要目的 token_type:为 bearer,这是 access token 最常用的一种形式 refresh_token:之后能够用这个值来交换新的 token,而不必输入账号暗码 expires_in:token 的过期时刻(秒)

咱们在用户客户端中界说了一个接口 http://localhost:6101/client-user/get,现在就拿着上一步获取的 token 来恳求这个接口

GET http://localhost:6101/client-user/get
Accept: */*
Cache-Control: no-cache
Authorization: bearer ce334918-e666-455a-8ecd-8bd680415d84

相同需求恳求头 Authorization,格局为 bearer + 空格 + token,正常情况下依据接口的逻辑,会把 token 原样回来

token 过期后,用 refresh_token 交换 access_token

一般都会设置 access_token 的过期时刻小于 refresh_token 的过期时刻,以便在 access_token 过期后,不必用户再次登录的情况下,获取新的 access_token

### 交换 access_token
POST http://localhost:6001/oauth/token?grant_type=refresh_token&refresh_token=706dac10-d48e-4795-8379-efe8307a2282
Accept: */*
Cache-Control: no-cache
Authorization: Basic dXNlci1jbGllbnQ6dXNlci1zZWNyZXQtODg4OA==

grant_type 设置为 refresh_token。

refresh_token 设置为恳求 token 时回来的 refresh_token 的值。

恳求头加入 Authorization,格局依然是 Basic + 空格 + base64(client-id:client-secret)

恳求成功后会回来和恳求 token 相同的数据格局

用 JWT 替换 redisToken

上面 token 的存储用的是 redis 的计划,Spring Security OAuth2 还提供了 jdbc 和 jwt 的支撑,jdbc 的暂不考虑,现在来介绍用 JWT 的办法来完结 token 的存储

用 JWT 的办法就不必把 token 再存储到服务端了,JWT 有自己特别的加密办法,能够有用的避免数据被篡改,只需不把用户暗码等要害信息放到 JWT 里就能够确保安全性

认证服务器改造:

先把有关 redis 的装备去掉,然后新增一个JwtTokenConfig类

@Configuration
public class JwtTokenConfig {
   @Bean
   public TokenStore jwtTokenStore() {
       return new JwtTokenStore(jwtAccessTokenConverter());
  }
   @Bean
   public JwtAccessTokenConverter jwtAccessTokenConverter() {
       JwtAccessTokenConverter accessTokenConverter = new JwtAccessTokenConverter();
       accessTokenConverter.setSigningKey("dev");
       return accessTokenConverter;
  }
}

JwtAccessTokenConverter是为了做 JWT 数据转换,这样做是因为 JWT 有自身独特的数据格局。假如没有了解过 JWT ,能够搜索一下先了解一下

更改后的OAuth装备类

@Autowired
private TokenStore jwtTokenStore;
@Autowired
private JwtAccessTokenConverter jwtAccessTokenConverter;
@Override
public void configure(final AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
       /**
        * 普通 jwt 形式
        */
        endpoints.tokenStore(jwtTokenStore)
              .accessTokenConverter(jwtAccessTokenConverter)
              .userDetailsService(kiteUserDetailsService)
               /**
                * 支撑 password 形式
                */
              .authenticationManager(authenticationManager);
}

注入 JWT 相关的 Bean,然后修正configure(final AuthorizationServerEndpointsConfigurer endpoints)办法为 JWT 存储形式

更改用户客户端

修正后的application.yml文件

security:
oauth2:
  client:
    client-id: user-client
    client-secret: user-secret-8888
    user-authorization-uri: http://localhost:6001/oauth/authorize
    access-token-uri: http://localhost:6001/oauth/token
  resource:
    jwt:
      key-uri: http://localhost:6001/oauth/token_key
      key-value: dev

注意认证服务端JwtAccessTokenConverter设置的 SigningKey 要和装备文件中的 key-value 相同,不然会导致无法正常解码 JWT ,导致验证不经过

资源服务装备

@Configuration
@EnableResourceServer
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
   @Bean
   public TokenStore jwtTokenStore() {
       return new JwtTokenStore(jwtAccessTokenConverter());
  }
   @Bean
   public JwtAccessTokenConverter jwtAccessTokenConverter() {
       JwtAccessTokenConverter accessTokenConverter = new JwtAccessTokenConverter();
       accessTokenConverter.setSigningKey("dev");
       accessTokenConverter.setVerifierKey("dev");
       return accessTokenConverter;
  }
   @Autowired
   private TokenStore jwtTokenStore;
   @Override
   public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
       resources.tokenStore(jwtTokenStore);
  }
}

以上便是 password 形式的完好过程,下一篇更新授权码形式