使用OAuth2實現認證服務器和資源服務器


在項目中有用到OAuth2,這里記錄下研究成功。詳細介紹可參考官方文檔:https://tools.ietf.org/html/rfc6749

 

准備工作:

1、spring-oauth-server 認證服務器和資源服務器(也可以分開)。作為一個jar包提供給客戶端使用

2、spring-security-demo 客戶端。資源所有者,需要依賴spring-oauth-server進行授權認證

 

spring-oauth-server

pom依賴:

<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>

RedisTokenStore配置:

@Configuration
public class RedisTokenStoreConfig {

    @Autowired
    private RedisConnectionFactory connectionFactory;

    /**
     * 配置Token存儲到Redis中
     */
    @Bean
    public TokenStore redisTokenStore() {
        return new RedisTokenStore(connectionFactory);
    }

}

兩個配置類SecurityProperty和OAuth2Property

@Getter
@Setter
@Configuration
@ConfigurationProperties(prefix = "xwj.security")
public class SecurityProperty {
    
    private OAuth2Property oauth2 = new OAuth2Property();

}
@Getter
@Setter
public class OAuth2Property {

    private OAuth2ClientProperty[] clients = {};

}

認證服務器配置:

/**
 * 配置認證服務器
 */
@Configuration
@EnableAuthorizationServer // 開啟認證服務
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {

    @Autowired
    private AuthenticationManager authenticationManager;
    @Autowired
    private UserDetailsService userDetailsService;
    @Autowired
    private SecurityProperty securityProperty;
    @Autowired
    private TokenStore tokenStore;

    /**
     * 用來配置授權(authorization)以及令牌(token)的訪問端點和令牌服務(token services)
     */
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        endpoints.tokenStore(tokenStore) // 配置存儲token的方式(默認InMemoryTokenStore)
                .authenticationManager(authenticationManager) // 密碼模式,必須配置AuthenticationManager,不然不生效
                .userDetailsService(userDetailsService); // 密碼模式,這里得配置UserDetailsService

        /*
         * pathMapping用來配置端點URL鏈接,有兩個參數,都將以 "/" 字符為開始的字符串
         * 
         * defaultPath:這個端點URL的默認鏈接
         * 
         * customPath:你要進行替代的URL鏈接
         */
        endpoints.pathMapping("/oauth/token", "/oauth/xwj");
    }

    /**
     * 用來配置客戶端詳情服務(給誰發送令牌)
     */
    @Override
    public void configure(final ClientDetailsServiceConfigurer clients) throws Exception {
        InMemoryClientDetailsServiceBuilder builder = clients.inMemory();
        OAuth2ClientProperty[] oauth2Clients = securityProperty.getOauth2().getClients();
        if (ArrayUtils.isNotEmpty(oauth2Clients)) {
            for (OAuth2ClientProperty config : oauth2Clients) {
                builder // 使用in-memory存儲
                        .withClient(config.getClientId()).secret(config.getClientSecret())
                        .accessTokenValiditySeconds(config.getAccessTokenValiditySeconds()) // 發出去的令牌有效時間(秒)
                        .authorizedGrantTypes("authorization_code", "client_credentials", "password", "refresh_token") // 該client允許的授權類型
                        .scopes("all", "read", "write") // 允許的授權范圍(如果是all,則請求中可以不要scope參數,否則必須加上scopes中配置的)
                        .autoApprove(true); // 自動審核
            }
        }
    }

}

認證服務器端點配置:

1、token模式默認存儲在內存中,服務重啟后就沒了。這里改為使用redis存儲,同時也可用於客戶端擴展集群

2、如果要使用密碼模式,必須得配置AuthenticationManager(原因可查看源碼AuthorizationServerEndpointsConfigurer的getDefaultTokenGranters方法)

3、在使用密碼模式時,如果用戶實現了UserDetailsService類,則在驗證用戶名密碼時,使用自定義的方法。因為在校驗用戶名密碼時,使用了DaoAuthenticationProvider中的retrieveUser方法(具體可參考AuthenticationManager、ProviderManager

4、默認獲取token的路徑是/oauth/token,通過pathMapping方法,可改變默認路徑

客戶端配置:

1、這里是從配置類中讀取clientId、clientSecret、有效期等,便於擴展

2、authorizedGrantTypes,授權認證類型,這里配置的是授權碼模式、客戶端模式、密碼模式、刷新token模式(還有一種簡化模式,這里不演示)

3、如果不配置autoApprove,那獲取授權碼時,需要手動點一下授權

 

資源服務器配置:

@Configuration
@EnableResourceServer // 開啟資源服務
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
    
    @Override
    public void configure(HttpSecurity http) throws Exception {
        super.configure(http);
    }

}

使用默認的配置,表示對所有資源都需要授權認證,即授權通過后可以訪問所有資源

 

spring-security-demo

pom依賴:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
</dependency>
<!-- druid 數據庫連接池 -->
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid</artifactId>
    <version>1.0.5</version>
</dependency>
<dependency>
    <groupId>com.xwj</groupId>
    <artifactId>spring-oauth-server</artifactId>
    <version>0.0.1-SNAPSHOT</version>
</dependency>

application.yml配置文件:

server:
  port: 80
    
spring:
  application:
    name: spring-security-demo #應用程序名稱
  #durid 數據庫連接池
  datasource: 
    driver-class-name: com.mysql.jdbc.Driver
    type: com.alibaba.druid.pool.DruidDataSource
    url: jdbc:mysql://127.0.0.1:3306/xwj?autoReconnect=true&failOverReadOnly=false&createDatabaseIfNotExist=true&useSSL=false&useUnicode=true&characterEncoding=utf8
    username: root
    password: 123456
  jpa:
    open-in-view: true
    hibernate:
      ddl-auto: update
      #show-sql: true
    properties:
      hibernate.dialect: org.hibernate.dialect.MySQL57InnoDBDialect
  redis:
    database: 2 #Redis數據庫索引(默認為0)
    host: localhost #Redis服務器地址
    port: 6379 
    password: ## 密碼(默認為空)
    pool:
      max-active: 8 #連接池最大連接數(使用負值表示沒有限制)
      max-wait: -1 #連接池最大阻塞等待時間(使用負值表示沒有限制)
      max-idle: 8  #連接池中的最大空閑連接
     
logging:
  level:
    #root: INFO
    #org.hibernate: INFO
    jdbc: off
    jdbc.sqltiming: debug
    com:
      xwj: debug
    
xwj:
  security:
    oauth2:
      storeType: redis
      jwtSignKey: 1234567890
      clients[0]:
        clientId: test
        clientSecret: testsecret
        accessTokenValiditySeconds: 1800
      clients[1]:
        clientId: myid
        clientSecret: mysecret
        accessTokenValiditySeconds: 3600

新建UserDetailsService的實現類MyUserDetailServiceImpl類:

/**
 * 如果使用密碼模式,需要實現UserDetailsService,用於覆蓋默認的InMemoryUserDetailsManager方法
 * 
 * 可以用來校驗用戶信息,並且可以添加自定義的用戶屬性
 */
@Service
public class MyUserDetailServiceImpl implements UserDetailsService {

    @Autowired
    private IUserService userService;

    /**
     * 根據username查詢用戶實體
     */
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        // 通過用戶名查詢數據
        AuthUserInfo userInfo = userService.findByUsername(username);
        if (userInfo == null) {
            throw new BadCredentialsException("User '" + username + "' not found");
        }

        // 用戶角色
        List<? extends GrantedAuthority> authorities = AuthorityUtils
                .commaSeparatedStringToAuthorityList("ROLE_" + userInfo.getRole());

        return new SocialUser(username, userInfo.getPassword(), true, true, true, true, authorities);
    }

}

新建一個密碼加密的配置類,用來實現PasswordEncoder(默認的加密方式是BCryptPasswordEncoder)

/**
 * 加密方式配置
 */
@Configuration
public class PasswordEncoderConfig {

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new PasswordEncoder() {
            @Override
            public String encode(CharSequence rawPassword) {
                return DigestUtils.md5DigestAsHex(rawPassword.toString().getBytes());
            }

            @Override
            public boolean matches(CharSequence rawPassword, String encodedPassword) {
                return encodedPassword.contentEquals(encode(rawPassword));
            }
        };
    }

}

用戶信息實體:

@Entity
@Getter
@Setter
public class AuthUserInfo {

    @Id
    @TableGenerator(name = "global_id_gen", allocationSize = 1)
    @GeneratedValue(strategy = GenerationType.TABLE, generator = "global_id_gen")
    private Long id;

    /** 用戶名 */
    private String username;

    /** 密碼 */
    private String password;

    /** 角色 */
    private String role;

}

數據訪問層,自己實現。造一條用戶數據(角色只能為USER,在獲取授權碼的時候需要擁有該角色的用戶進行表單登錄):

新建一個IndexController:

@RestController
@RequestMapping("index")
public class IndexController {

    /**
     * 獲取資源
     */
    @GetMapping("/getResource")
    public String getResource() {
        return "OK";
    }

    /**
     * 獲取當前授權用戶
     */
    @GetMapping("/me")
    public Object getCurrrentUser(@AuthenticationPrincipal UserDetails user) {
        return user;
    }

}

 

1、授權碼模式:

   1.1、 瀏覽器請求如下地址,獲取授權code(其中response_type=code是固定寫法,scope為權限,state為自定義數據):

http://localhost/oauth/authorize?client_id=test&redirect_uri=http://www.baidu.com&response_type=code&scope=read
&state=mystate

    1.2、輸入用戶名密碼(xwj/123456):

   1.3、上面配置的自動授權,所有會oauth會立馬調用回調地址並返回授權code和state(可以發現state傳的什么就返回什么):

   1.4、在獲得授權碼后,接下來獲取訪問令牌。使用postman請求  http://localhost/oauth/xwj:

     注意,需要在Authorization里設置Username和Password(就是客戶端配置的clientId和clientSecret),還有TYPE類型:

  獲取到的token如下:

1.5、授權碼用一次之后,oauth將會把它從緩存中刪掉,所以只能使用一次。如果重復使用,將返回:

1.6、如果不帶上token,請求資源:http://localhost/index/getResource,將會返回無權訪問:

 1.7、如果帶上token,請求資源:http://localhost/index/getResource,將可以正常獲取資源數據:

   1.8、如果token錯誤,將提示無效的token:

 

2、客戶端模式:

  2.1、直接獲取token,請求地址:http://localhost/oauth/xwj

 

 獲取token操作同上。由於客戶端模式每次的參數是一樣的,則請求多次返回同一個token,只是有效期在變小

 

3、密碼模式:

  3.1、直接獲取token,請求地址:http://localhost/oauth/xwj

  獲取token操作同上。由於客戶端模式每次的參數是一樣的,則請求多次返回同一個token,只是有效期在變小

 

4、刷新token:

  4.1、以授權碼模式為例,在授權碼的token過期后,使用當時的refresh_token獲取新的token:

 

 4.2、獲取到新的token,就可以正常訪問資源了:

 

使用redis存儲token,打開Redis Desktop Manager工具,可以看到數據結構如下:

 

 

 

至此,演示完畢~~~

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM