在項目中有用到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工具,可以看到數據結構如下:

至此,演示完畢~~~
