spring cloud oauth2 实现用户认证登陆

spring-cloud-oauth2 实现用户认证及单点登陆

需求

​ 在微服务架构中,咱们有不少业务模块,每一个模块都须要有用户认证,权限校验。有时候也会接入来自第三方厂商的应用。要求是只登陆一次,便可在各个服务的受权范围内进行操做。看到这个需求,立马就想到了这不就是单点登陆吗?因而基于这样的需求,做者使用spring-cloud-oauth2去简单的实现了下用户认证和单点登陆。html

相关介绍

OAuth2

OAuth2是一个关于受权的网络标准,他定制了设计思路和执行流程。OAuth2一共有四种受权模式:受权码模式(authorization code)、简化模式(implicit)、密码模式(resource owner password)和客户端模式(client credentials)。数据的全部者告诉系统赞成受权第三方应用进入系统,获取这些数据。因而数据全部者生产了一个短期内有效的受权码(token)给第三方应用,用来代替密码,供第三方使用。具体流程请看下图,具体的OAuth2介绍,能够参考这篇文章,写的很详细。(http://www.ruanyifeng.com/blog/2019/04/oauth_design.html)java

img

Token

令牌(token)和密码(password)的做用是同样的,均可以进入系统获取资源,可是也有几点不一样:mysql

  1. 令牌是短时间的,到期会自动失效,用户没法修改。密码是长期的,用户能够修改,若是不修改,就不会发生变化。
  2. 令牌能够被数据全部者撤销,令牌会当即失效。密码通常不容许其余人撤销,只能被操做权限更高的人或者本人修改/重制。
  3. 令牌是有权限范围的,会被数据全部者授予。

实现的功能

本篇介绍的是经过密码模式来实现单点登陆的功能。web

​ 在微服务架构中,咱们的一个应用可能会有不少个服务运行,协调来处理实际的业务。这就须要用到单点登陆的技术,来统一认证调取接口的是哪一个用户。那总不能请求一次,就认证一次,这么作确定是不行的。那么就须要在认证完用户以后,给这个用户受权,而后发一个令牌(token),有效期内用户请求资源时,就只须要带上这个标识本身身份的token便可。redis

架构说明

认证中心:oauth2-oauth-server,OAuth2的服务端,主要完成用户Token的生成、刷新、验证等。spring

微服务:mzh-etl,微服务之一,接收到请求以后回到认证中心(oauth2-oauth-server)去验证。sql

代码实现

使用到的框架是java基础的spring boot 和spring-cloud-oauth2数据库

认证中心:
一、引入须要的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。json

二、配置application.yml

这里主要用到的是redis的配置,mysql数据库的配置暂时没有用到。缓存

spring:
  application:
    name: oauth-server
  datasource:
    url: jdbc:mysql://localhost:3306/mzh_oauth?useSSL=false&characterEncoding=UTF-8
    username: root
    password: admin123
    driver-class-name: com.mysql.jdbc.Driver
    hikari:
      connection-timeout: 30000
      idle-timeout: 600000
      max-lifetime: 1800000
      maximum-pool-size: 9
  redis:
    database: 0
    host: localhost
    port: 6379
    jedis:
      pool:
        max-active: 8
        max-idle: 8
        min-idle: 0
    timeout: 10000
server:
  port: 8888
  use-forward-headers: true

management:
  endpoint:
    health:
      enabled: true
三、spring security 权限配置

须要继承WebSecurityConfigurerAdapter

/**
 * @Author mzh
 * @Date 2020/10/24
 */
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private CustomUserDetailsService customUserDetailsService;
  
    /**
     * 修改密码的加密方式
     * @return
     */
    @Bean
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }

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

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception{
       // 若是使用BCryptPasswordEncoder,这里就必须指定密码的加密类
       auth.userDetailsService(customUserDetailsService).passwordEncoder(passwordEncoder());
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .antMatchers("/oauth/**").permitAll();
    }
}

BCryptPasswordEncoder是一个不可逆的密码加密类,AuthenticationManager是OAuth2的password必须指定的受权管理Bean。

CustomUserDetailsService这个类是被注入进来的,熟悉spring security的同窗应该知道,spring security有一个本身的UserdetailsService用于权限校验时获取用户信息,可是不少时候不符合咱们的业务场景,就须要重现实现这个类。

四、实现CustomUserDetailsService

UserDetailsService这个类的核心方法就是loadUserByUsername()方法,他接收一个用户名,返回一个UserDetails对象。

/**
 * @Author mzh
 * @Date 2020/10/24
 */
@Component(value = "customUserDetailsService")
public class CustomUserDetailsService implements UserDetailsService {

    @Autowired
    private PasswordEncoder passwordEncoder;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        // 1. 根据username 去数据库查询 user

        // 2.获取用户的角色和权限

        // 下面是写死,暂时不和数据库交互
        if(!(("admin").equals(username))){
            throw new UsernameNotFoundException("the user is not found");
        }else{
            String role = "ADMIN_ROLE";
            List<SimpleGrantedAuthority> authorities = new ArrayList<>();
            authorities.add(new SimpleGrantedAuthority(role));
            String password = passwordEncoder.encode("123456");
            return new User(username,password,authorities);
        }
    }
}

这里是在程序中写死了用户和权限。帐号:admin,密码:123456,权限:ADMIN_ROLE(注意是权限,不是角色),实际中应该从数据库获取用户和相关的权限,而后进行认证。

五、OAuth2 配置

OAuth2配置须要继承AuthorizationServerConfigurerAdapter

/**
 * @Author mzh
 * @Date 2020/10/24
 */
@Configuration
@EnableAuthorizationServer
public class Oauth2Config extends AuthorizationServerConfigurerAdapter {

    @Autowired
    private PasswordEncoder passwordEncoder;

    @Autowired
    private UserDetailsService customUserDetailsService;

    @Autowired
    private AuthenticationManager authenticationManager;

    @Autowired
    private TokenStore redisTokenStore;

    /**
     * 对AuthorizationServerEndpointsConfigurer参数的重写
     * 重写受权管理Bean参数
     * 重写用户校验
     * 重写token缓存方式
     * @param endpointsConfigurer
     * @throws Exception
     */
    @Override
    public void configure(final AuthorizationServerEndpointsConfigurer endpointsConfigurer) throws Exception{
        endpointsConfigurer.authenticationManager(authenticationManager)
                .userDetailsService(customUserDetailsService)
                .tokenStore(redisTokenStore);
    }

    /**
     * 客户端的参数的重写
     * 这里是将数据直接写入内存,实际应该从数据库表获取
     * clientId:客户端Id
     * secret:客户端的密钥
     * authorizedGrantTypes:受权方式
     *     authorization_code: 受权码类型,
     *     implicit: 隐式受权,
     *     password: 密码受权,
     *     client_credentials: 客户端受权,
     *     refresh_token: 经过上面4中方式获取的刷新令牌获取的新令牌,
     *                      注意是获取token和refresh_token以后,经过refresh_toke刷新以后的令牌
     * accessTokenValiditySeconds: token有效期
     * scopes 用来限制客户端访问的权限,只有在scopes定义的范围内,才能够正常的换取token
     * @param clients
     * @throws Exception
     */
    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception{
        clients.inMemory()
                .and()
                .withClient("mzh-etl")
                .secret(passwordEncoder.encode("mzh-etl-8888"))
                .authorizedGrantTypes("refresh_token","authorization_code","password")
                .accessTokenValiditySeconds(3600)
                .scopes("all");
    }

    @Override
    public void configure(AuthorizationServerSecurityConfigurer serverSecurityConfigurer) throws Exception{
        serverSecurityConfigurer.allowFormAuthenticationForClients();
        serverSecurityConfigurer.checkTokenAccess("permitAll()");
        serverSecurityConfigurer.tokenKeyAccess("permitAll()");
        serverSecurityConfigurer.passwordEncoder(passwordEncoder);
    }
}
六、启动服务

上述步骤完成以后启动服务,而后观察IDEA下方的Endpoints中的Mappings,就能够找到相关的认证端口。主要的有如下几个:

POST /oauth/authorize  受权码模式认证受权接口 
GET/POST /oauth/token  获取 token 的接口 
POST  /oauth/check_token  检查 token 合法性接口

到此,认证中心就算是建立完成了。咱们经过idea的REST Client 来请求一个token进行测试。

请求内容以下:

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

第一行POST http://localhost:8888/oauth/token?grant_type=password&username=admin&password=123456&scope=all 表示发起一个POST请求,请求路径是/oauth/token,请求参数是grant_type=password表示认证类型是password,username=admin&password=123456表示用户名是admin,密码是123456scope=all是权限相关的,以前在Oauth2Config 中配置了scope是all。

第四行表示在请求头中加入一个字段Authorization,值为Basic空格base64(clientId:clientSecret),咱们以前配置的clientId是“meh-etl”,clientSecret是"meh-etl-8888",因此这个值的base64是:bXpoLWV0bDptemgtZXRsLTg4ODg=

运行请求以后,若是参数都正确的话,获取到返回的内容以下:

{
  // token值,后面请求接口时都须要带上的token
	"access_token": "b4cb804c-93d2-4635-913c-265ff4f37309",
  // token的形式
  "token_type": "bearer",
  // 快过时时能够用这个换取新的token
  "refresh_token": "5cac05f4-158f-4561-ab16-b06c4bfe899f",
  // token的过时时间
	"expires_in": 3599,
  // 权限范围
	"scope": "all"
}

token值过时以后,能够经过refresh_token来换取新的access_token

POST http://localhost:8888/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值,也就是以前请求获取Token的refresh_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>
二、配置application.yml
spring:
  application:
    name: mzh-etl
  redis:
    database: 1
    host: localhost
    port: 6379
    jedis:
      pool:
        max-active: 8
        max-idle: 8
        min-idle: 0
    timeout: 10000
server:
  port: 8889
security:
  oauth2:
    client:
      # 须要和以前认证中心配置中的同样
      client-id: mzh-etl
      client-secret: mzh-etl-8888
      # 获取token的地址
      access-token-uri: http://localhost:8888/oauth/token
    resource:
      id: mzh-etl
      user-info-uri: user-info
    authorization:
      # 检查token的地址
      check-token-access: http://localhost:8888/oauth/check_token

这里的配置必定要仔细,必须和以前认证中心中配置的同样。

三、资源配置

在OAuth2中接口也称为资源,资源的权限也就是接口的权限。spring-cloud-oauth2提供了关于资源的注解@EnableResourceServer

/**
 * @Author mzh
 * @Date 2020/10/24
 */
@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 clientSecret;

    @Value("${security.oauth2.authorization.check-token-access}")
    private String checkTokenEndpointUrl;

    @Autowired
    private RedisConnectionFactory redisConnectionFactory;

    @Bean("redisTokenStore")
    public TokenStore redisTokenStore(){
        return new RedisTokenStore(redisConnectionFactory);
    }

    @Bean
    public RemoteTokenServices tokenService() {
        RemoteTokenServices tokenService = new RemoteTokenServices();
        tokenService.setClientId(clientId);
        tokenService.setClientSecret(clientSecret);
        tokenService.setCheckTokenEndpointUrl(checkTokenEndpointUrl);
        return tokenService;
    }

    @Override
    public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
        resources.tokenServices(tokenService());
    }

    @Override
    public void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests().antMatchers("/get/**").authenticated();
    }
}
四、建立一个接口
@RestController
public class UserController {

    @GetMapping("get")
    @PreAuthorize("hasAuthority('ADMIN_ROLE')")
    public Object get(Authentication authentication){
        authentication.getAuthorities();
        OAuth2AuthenticationDetails details = (OAuth2AuthenticationDetails) authentication.getDetails();
        String token = details.getTokenValue();
        return token;
    }
}

这个接口就是会返回一个请求他时携带的token值,@PreAuthorize会在请求接口时检查是否用权限“ADMIN_ROLE”(以前认证中心配置的权限)

五、启动服务

启动服务,只有当用户有“ADMIN_ROLE“的时候,才能正确返回,不然返回401未受权

一样适用REST Client来发起一个请求:

GET http://localhost:8889/get 
Accept: */* 
Cache-Control: no-cache 
Authorization: bearer b4cb804c-93d2-4635-913c-265ff4f37309

请求路径是http://localhost:8889/get 而后在请求头部带上咱们上一步骤获取到的token,放入到Authorization中,格式是bearer空格token值,若是请求成功,就会把token原样返回。

本文由博客群发一文多发等运营工具平台 OpenWrite 发布

相关文章
相关标签/搜索