單點登入實作之基于OAuth2.0協定
- OAuth2
-
- 開發流程
- OAuth2實作單點登入
-
- 基本介紹
- 相關配置
-
- 認證伺服器配置
-
- 依賴配置
- AuthorizationServerConfig(自定義認證配置類)
-
- 介紹
- 案例代碼
- WebSecurityConfig(自定義安全配置類)
-
- 介紹
- 案例代碼
- 用戶端詳細資訊表(官方)
- 核心配置檔案
- 用戶端配置
-
- 依賴配置
- WebSecurityConfig(自定義安全配置類)
- ResourceServerConfig(資源配置類)
- 介紹
- 注意事項
- 案例代碼
- 參考連結
OAuth2
-
概念
OAuth(Open Authorization,開放授權)是為使用者資源的授權定義了一個安全、開放及簡單的标準,第三方無需知道使用者的賬号及密碼,就可擷取到使用者的授權資訊。
OAuth2.0是OAuth協定的延續版本,但不向後相容OAuth 1.0即完全廢止了OAuth1.0
-
應用場景
第三方應用授權登入:在APP或者網頁接入一些第三方應用時,時常會需要使用者登入另一個合作平台,比如QQ,微網誌,微信的授權登入,第三方應用通過oauth2方式擷取使用者資訊
開發流程
- 微信開發文檔流程說明
- 第三方發起微信授權登入請求,微信使用者允許授權第三方應用後,微信會拉起應用或重定向到第三方網站,并且帶上授權臨時票據code參數;
- 通過code參數加上AppID和AppSecret等,通過API換取access_token;
- 通過access_token進行接口調用,擷取使用者基本資料資源或幫助使用者實作基本操作。
- 授權登入流程圖
單點登入實作之基于OAuth2.0協定OAuth2OAuth2實作單點登入參考連結
OAuth2實作單點登入
基本介紹
-
實作原理
在OAuth2在有授權伺服器、資源伺服器、用戶端這樣幾個角色,當我們用它來實作SSO的時候是不需要資源伺服器這個角色的,有授權伺服器和用戶端就夠了。
授權伺服器當然是用來做認證的,用戶端就是各個應用系統,我們隻需要登入成功後拿到使用者資訊以及使用者所擁有的權限即可
-
注意事項
之前我一直認為把那些需要權限控制的資源放到資源伺服器裡保護起來就可以實作權限控制,其實是我想錯了,權限控制還得通過Spring Security或者自定義攔截器來做
-
概念介紹
SSO:是一種思想,或者說是一種解決方案,是抽象的,我們要做的就是按照它的這種思想去實作它
OAuth2:是用來允許使用者授權第三方應用通路他在另一個伺服器上的資源的一種協定,它不是用來做單點登入的,但我們可以利用它來實作單點登入。在本例實作SSO的過程中,受保護的資源就是使用者的資訊(包括,使用者的基本資訊,以及使用者所具有的權限),而我們想要通路這這一資源就需要使用者登入并授權,OAuth2服務端負責令牌的發放等操作
JWT:令牌的生成我們采用JWT,也就是說JWT是用來承載使用者的Access_Token的
Spring Security:是用于安全通路的,這裡我們我們用來做通路權限控制
相關配置
認證伺服器配置
依賴配置
- pom.xml
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.1.3.RELEASE</version> <relativePath/> <!-- lookup parent from repository --> </parent> <groupId>com.cjs.sso</groupId> <artifactId>oauth2-sso-auth-server</artifactId> <version>0.0.1-SNAPSHOT</version> <name>oauth2-sso-auth-server</name> <properties> <java.version>1.8</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <!-- spring-cloud-starter-oauth2包含了 spring-cloud-starter-security,不用再單獨引入 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <!-- 重要依賴 --> <dependency> <groupId>org.springframework.security.oauth.boot</groupId> <artifactId>spring-security-oauth2-autoconfigure</artifactId> <version>2.1.3.RELEASE</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.session</groupId> <artifactId>spring-session-data-redis</artifactId> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-lang3</artifactId> <version>3.8.1</version> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2.56</version> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>
AuthorizationServerConfig(自定義認證配置類)
介紹
-
參考連結
https://blog.csdn.net/qq_33460562/article/details/79351938
-
AuthorizationServerConfigurerAdapter類
AuthorizationServerConfigurerAdapter類用于配置授權的相關資訊,屬于OAuth2 配置檔案,是配置的核心,主要配置用戶端,配置token存儲方式等
- @EnableAuthorizationServer注解
@Target({ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) @Documented @Import({AuthorizationServerEndpointsConfiguration.class, AuthorizationServerSecurityConfiguration.class}) public @interface EnableAuthorizationServer { }
- configure(AuthorizationServerSecurityConfigurer oauthServer)方法
//用來配置令牌端點的安全限制 @Override public void configure(AuthorizationServerSecurityConfigurer oauthServer) throws Exception { oauthServer.allowFormAuthenticationForClients();//允許用戶端通路 OAuth2 授權接口,否則請求 token 會傳回 401。 oauthServer.checkTokenAccess("isAuthenticated()");//允許已授權使用者通路 checkToken 接口 oauthServer.tokenKeyAccess("isAuthenticated()");//允許已授權使用者通路擷取 token 接口 }
- configure(ClientDetailsServiceConfigurer clients)方法
- 記憶體中配置用戶端詳情資訊
//用來配置用戶端詳情服務(ClientDetailsService),用戶端詳情資訊在這裡進行初始化,你能夠把用戶端詳情資訊寫死在這裡或者是通過資料庫來存儲調取詳情資訊 @Override public void configure(ClientDetailsServiceConfigurer clients) throws Exception { //使用存在記憶體中配置兩個用戶端應用的通行證,相當于寫死了,正式環境下的做法是持久化到資料庫中,比如 mysql 中 clients.inMemory() .withClient("sheep1") .secret(new BCryptPasswordEncoder().encode("123456")) .authorizedGrantTypes("authorization_code", "refresh_token") .redirectUris("http://localhost:8086/login") .scopes("all") .autoApprove(false) .and() .withClient("sheep2") .secret(new BCryptPasswordEncoder().encode("123456")) .authorizedGrantTypes("authorization_code", "refresh_token") .redirectUris("http://localhost:8087/login") .scopes("all") .autoApprove(false); }
- 官方資料庫結構中配置用戶端詳情資訊
@Autowired private DataSource dataSource; //用來配置用戶端詳情服務(ClientDetailsService),用戶端詳情資訊在這裡進行初始化,你能夠把用戶端詳情資訊寫死在這裡或者是通過資料庫來存儲調取詳情資訊 @Override public void configure(ClientDetailsServiceConfigurer clients) throws Exception { clients.jdbc(dataSource); } //資料庫表結構見下面
- 自定義資料庫結構中配置用戶端詳情資訊
//用來配置用戶端詳情服務(ClientDetailsService),用戶端詳情資訊在這裡進行初始化,可以通過自定義資料庫來存儲調取詳情資訊 @Override public void configure(ClientDetailsServiceConfigurer clients) throws Exception { // 配置用戶端應用通信證, 用于client認證 clients.withClientDetails(getClientDetails()); } @Bean // 聲明ApplyClientDetailService public ApplyClientDetailService getClientDetails() { return new ApplyClientDetailService(); }
ApplyClientDetailService自定義擷取用戶端詳情類
import org.apache.tomcat.jdbc.pool.DataSource; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.oauth2.provider.ClientDetails; import org.springframework.security.oauth2.provider.ClientDetailsService; import org.springframework.security.oauth2.provider.ClientRegistrationException; import org.springframework.security.oauth2.provider.client.JdbcClientDetailsService; import oauth.security.client.service.ApplyService; /** * * 1. 在配置用戶端中,使用了ApplyClientDetailService類,是自定義的擷取Client的一個類,實作ClientDetailsService接口, * 重寫loadClientByClientId方法 * * 2. 對Client的通路主要依靠JdbcClientDetailsService類的實作,必須使用官方給出的資料庫結構, * 如果想自定義資料庫結構,可以根據需求重寫ClientDetailsService類的實作 **/ public class ApplyClientDetailService implements ClientDetailsService { @Autowired private ApplyService applyService; @Autowired private DataSource dataSource; @Override public ClientDetails loadClientByClientId(String applyName) throws ClientRegistrationException { /* // 使用mybatic驗證client是否存在 ,根據需求寫sql Map clientMap = applyService.findApplyById(applyName); if(clientMap == null) { throw new ClientRegistrationException("應用" + applyName + "不存在!"); }*/ // MyJdbcClientDetailsService jdbcClientDetailsService= new MyJdbcClientDetailsService(dataSource, "authentication"); JdbcClientDetailsService jdbcClientDetailsService= new JdbcClientDetailsService(dataSource); ClientDetails clientDetails = jdbcClientDetailsService.loadClientByClientId(applyName); return clientDetails; } }
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。
- configure(AuthorizationServerEndpointsConfigurer endpoints)方法
- 資料庫儲存token
@Autowired AuthenticationManager authenticationManager; @Autowired private DataSource dataSource; // 初始化JdbcTokenStore @Autowired public TokenStore getTokenStore() { return new JdbcTokenStore(dataSource); } //用來配置令牌(token)的通路端點和令牌服務(token services) @Override public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception { endpoints .tokenStore(getTokenStore()) // 資料庫儲存token .authenticationManager(authenticationManager); //調用此方法才能支援 password 模式。 }
- Redis儲存token
@Autowired AuthenticationManager authenticationManager; @Autowired RedisConnectionFactory redisConnectionFactory; //用來配置令牌(token)的通路端點和令牌服務(token services) @Override public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception { endpoints .tokenStore(new RedisTokenStore(redisConnectionFactory))// 指定 token 的存儲方式,redis儲存token .authenticationManager(authenticationManager); //調用此方法才能支援 password 模式。 }
- 普通JWT儲存token
@Autowired AuthenticationManager authenticationManager; @Autowired public UserDetailsService nbspUserDetailsService; //用來配置令牌(token)的通路端點和令牌服務(token services) @Override public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception { endpoints .tokenStore(jwtTokenStore()) // 指定 token 的存儲方式,普通jwt方式存儲 .accessTokenConverter(jwtAccessTokenConverter()) .userDetailsService(nbspUserDetailsService)//設定使用者驗證服務,自定義使用者實作類實作 UserDetailsService接口,重寫loadUserByUsername方法,傳回使用者資訊對象 .authenticationManager(authenticationManager);//調用此方法才能支援 password 模式 } @Bean public TokenStore jwtTokenStore() { return new JwtTokenStore(jwtAccessTokenConverter()); } @Bean public JwtAccessTokenConverter jwtAccessTokenConverter() { JwtAccessTokenConverter accessTokenConverter = new JwtAccessTokenConverter(); accessTokenConverter.setSigningKey("dev"); return accessTokenConverter; }
- 增強JWT儲存token
// 由于JWT配置已經抽取至JwtTokenConfig配置類,項目啟動後預先加載,這裡可以直接注入 @Autowired private TokenStore jwtTokenStore; @Autowired//預設按類型注入,若存在多個執行個體,需要結合@Qualifier注解指定注入Bean的名稱 private JwtAccessTokenConverter jwtAccessTokenConverter; @Autowired private TokenEnhancer jwtTokenEnhancer; @Autowired AuthenticationManager authenticationManager; @Autowired public UserDetailsService nbspUserDetailsService; //用來配置令牌(token)的通路端點和令牌服務(token services) @Override public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception { TokenEnhancerChain enhancerChain = new TokenEnhancerChain(); List<TokenEnhancer> enhancerList = new ArrayList<>(); enhancerList.add(jwtTokenEnhancer); enhancerList.add(jwtAccessTokenConverter); enhancerChain.setTokenEnhancers(enhancerList); endpoints.tokenStore(jwtTokenStore)// 指定 token 的存儲方式,增強jwt方式存儲 .accessTokenConverter(jwtAccessTokenConverter) .tokenEnhancer(enhancerChain) .userDetailsService(nbspUserDetailsService)//設定使用者驗證服務,自定義使用者實作類實作 UserDetailsService接口,重寫loadUserByUsername方法,傳回使用者資訊對象 .authenticationManager(authenticationManager);//調用此方法才能支援 password 模式 }
JwtTokenConfig配置類
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.oauth2.provider.token.TokenEnhancer; import org.springframework.security.oauth2.provider.token.TokenStore; import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter; import org.springframework.security.oauth2.provider.token.store.JwtTokenStore; //自動注入JWT配置 @Configuration public class JwtTokenConfig { @Bean//@Bean就放在方法上,就是讓方法去産生一個Bean,然後交給Spring容器 public TokenStore jwtTokenStore() { return new JwtTokenStore(jwtAccessTokenConverter()); } @Bean public JwtAccessTokenConverter jwtAccessTokenConverter() { JwtAccessTokenConverter accessTokenConverter = new JwtAccessTokenConverter(); accessTokenConverter.setSigningKey("dev");// Sets the JWT signing key return accessTokenConverter; } @Bean public TokenEnhancer jwtTokenEnhancer(){ return new JWTokenEnhancer(); } }
JWTokenEnhancer增強配置類
import org.springframework.security.oauth2.common.DefaultOAuth2AccessToken; import org.springframework.security.oauth2.common.OAuth2AccessToken; import org.springframework.security.oauth2.provider.OAuth2Authentication; import org.springframework.security.oauth2.provider.token.TokenEnhancer; import java.util.HashMap; import java.util.Map; /** * 如果想在 JWT 中加入額外的字段(比方說使用者的其他資訊), * 需要自定義增強實作類,實作 spring security oauth2提供的 TokenEnhancer 增強器接口 * 重寫enhance方法(RedisToken 的方式同樣可以) * */ public class JWTokenEnhancer implements TokenEnhancer { @Override public OAuth2AccessToken enhance(OAuth2AccessToken oAuth2AccessToken, OAuth2Authentication oAuth2Authentication) { Map<String, Object> info = new HashMap<>(); info.put("jwt-ext", "JWT 擴充資訊"); ((DefaultOAuth2AccessToken) oAuth2AccessToken).setAdditionalInformation(info); return oAuth2AccessToken; } }
- 常見自定義重寫實作類
- 重寫UserDetailsService接口實作類
import com.alibaba.fastjson.JSON; import com.cjs.sso.domain.MyUser; import com.cjs.sso.entity.SysPermission; import com.cjs.sso.entity.SysUser; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; import org.springframework.util.CollectionUtils; import java.util.ArrayList; import java.util.List; /** * * 一般需要重寫,自定義使用者詳情實作類,需要實作UserDetailsService接口,重寫loadUserByUsername方法, * 通過使用者名查找使用者資訊,并傳回使用者詳情資訊,包括使用者名,加密後的密碼,和權限集合 * */ @Slf4j @Service public class NbspUserDetailsService implements UserDetailsService { @Autowired private PasswordEncoder passwordEncoder; @Autowired private UserService userService; @Autowired private PermissionService permissionService; @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { SysUser sysUser = userService.getByUsername(username); if (null == sysUser) { log.warn("使用者{}不存在", username); throw new UsernameNotFoundException(username); } List<SysPermission> permissionList = permissionService.findByUserId(sysUser.getId()); List<SimpleGrantedAuthority> authorityList = new ArrayList<>(); if (!CollectionUtils.isEmpty(permissionList)) { for (SysPermission sysPermission : permissionList) { authorityList.add(new SimpleGrantedAuthority(sysPermission.getCode())); } } MyUser myUser = new MyUser(sysUser.getUsername(), passwordEncoder.encode(sysUser.getPassword()), authorityList); log.info("登入成功!使用者: {}", JSON.toJSONString(myUser)); return myUser; //傳回預設使用者詳情資訊 //return new org.springframework.security.core.userdetails.User(username,password, authorities); } }
- 重寫User類
import lombok.Data; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.userdetails.User; import java.util.Collection; /** * 大部分時候直接用User即可不必擴充 * */ @Data public class MyUser extends User { private Integer departmentId; // 舉個例子,部門ID private String mobile; // 舉個例子,假設我們想增加一個字段,這裡我們增加一個mobile表示手機号 public MyUser(String username, String password, Collection<? extends GrantedAuthority> authorities) { super(username, password, authorities); } public MyUser(String username, String password, boolean enabled, boolean accountNonExpired, boolean credentialsNonExpired, boolean accountNonLocked, Collection<? extends GrantedAuthority> authorities) { super(username, password, enabled, accountNonExpired, credentialsNonExpired, accountNonLocked, authorities); } }
案例代碼
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.security.core.token.DefaultToken;
import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer;
import org.springframework.security.oauth2.provider.token.DefaultTokenServices;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;
import org.springframework.security.oauth2.provider.token.store.JwtTokenStore;
import javax.sql.DataSource;
/**
* @author CharlesYan
* @date 2020-04-17
*/
@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
@Autowired
private DataSource dataSource;
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
security.allowFormAuthenticationForClients();
security.tokenKeyAccess("isAuthenticated()");
}
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
//普通資料方式擷取使用者詳情資訊
clients.jdbc(dataSource);
}
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
//普通jwt方式儲存token
endpoints.accessTokenConverter(jwtAccessTokenConverter());
endpoints.tokenStore(jwtTokenStore());
// endpoints.tokenServices(defaultTokenServices());
}
/*@Primary
@Bean
public DefaultTokenServices defaultTokenServices() {
DefaultTokenServices defaultTokenServices = new DefaultTokenServices();
defaultTokenServices.setTokenStore(jwtTokenStore());
defaultTokenServices.setSupportRefreshToken(true);
return defaultTokenServices;
}*/
@Bean
public JwtTokenStore jwtTokenStore() {
return new JwtTokenStore(jwtAccessTokenConverter());
}
@Bean
public JwtAccessTokenConverter jwtAccessTokenConverter() {
JwtAccessTokenConverter jwtAccessTokenConverter = new JwtAccessTokenConverter();
jwtAccessTokenConverter.setSigningKey("cjs"); // Sets the JWT signing key
return jwtAccessTokenConverter;
}
}
WebSecurityConfig(自定義安全配置類)
介紹
-
參考連結
https://blog.csdn.net/sinat_29899265/article/details/80736498
-
WebSecurityConfigurerAdapter類
WebSecurityConfigurerAdapter類是個擴充卡,屬于spring security 基礎配置,在配置的時候,需要我們自己寫個配置類去繼承它,然後編寫自己所特殊需要的配置
- @EnableWebSecurity注解
//@EnableWebSecurity完成的工作便是加載了WebSecurityConfiguration,AuthenticationConfiguration這兩個核心配置類,也就此将spring security的職責劃分為了配置安全資訊,配置認證資訊兩部分 @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.TYPE}) @Documented @Import({WebSecurityConfiguration.class, SpringWebMvcImportSelector.class, OAuth2ImportSelector.class}) @EnableGlobalAuthentication @Configuration public @interface EnableWebSecurity { boolean debug() default false; }
- configure(HttpSecurity http)方法
@Configuration @EnableWebSecurity public class WebSecurityConfig extends WebSecurityConfigurerAdapter { //1. 配置Security的認證政策(定義授權規則), 每個子產品配置使用and結尾 @Override protected void configure(HttpSecurity http) throws Exception { http .authorizeRequests()//2. authorizeRequests()配置路徑攔截,表明路徑通路所對應的權限,角色,認證資訊。 .antMatchers("/resources/**", "/signup", "/about").permitAll() .antMatchers("/admin/**").hasRole("ADMIN") .antMatchers("/db/**").access("hasRole('ADMIN') and hasRole('DBA')") .anyRequest().authenticated() .and() .formLogin()//3. formLogin()對應表單認證相關的配置 .usernameParameter("username") .passwordParameter("password") .failureForwardUrl("/login?error") .loginPage("/login") .permitAll() .and() .logout()//4. logout()對應了登出相關的配置 .logoutUrl("/logout") .logoutSuccessUrl("/index") .permitAll() .and() .httpBasic()//5. httpBasic()可以配置basic登入 .disable(); } }
- configure(AuthenticationManagerBuilder auth)方法
- 在記憶體中配置使用者認證資訊
@Configuration @EnableWebSecurity public class WebSecurityConfig extends WebSecurityConfigurerAdapter { //1. 配置的是認證資訊(定義認證規則) //作用:AuthenticationManagerBuilder 這個類就是AuthenticationManager的建造者, 我們隻需要向這個類中, 配置使用者資訊, 就能生成對應的AuthenticationManager, 這個類是使用者身份的管理者, 是認證的入口, 是以,我們需要通過這個配置,向security提供真實的使用者身份。 @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth .inMemoryAuthentication()//2. 設定從緩存中擷取已事先定義好的認證資訊(配置一個記憶體中的使用者認證器) .withUser("admin").password("admin").roles("USER"); } }
- 資料庫中擷取認證資訊
@Configuration @EnableWebSecurity public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private NbspUserDetailsService userDetailsService; @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } //1. 配置的是認證資訊(定義認證規則) //作用:AuthenticationManagerBuilder 這個類就是AuthenticationManager的建造者, 我們隻需要向這個類中, 配置使用者資訊, 就能生成對應的AuthenticationManager, 這個類是使用者身份的管理者, 是認證的入口, 是以,我們需要通過這個配置,向security提供真實的使用者身份。 @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth .userDetailsService(userDetailsService)//3. userDetailsService是使用者自定義使用者詳情服務類, 這個類的作用就是去擷取使用者資訊,比如從資料庫中擷取。 這樣的話,AuthenticationManager在認證使用者身份資訊的時候,就會從中擷取使用者身份,和從http中拿的使用者身份做對比 .passwordEncoder(passwordEncoder()); } //注意事項:UserDetailService會預設加載DaoAuthenticationProvider }
- configure(WebSecurity web)方法
@Configuration @EnableWebSecurity public class WebSecurityConfig extends WebSecurityConfigurerAdapter { //全局請求忽略規則配置(比如說靜态檔案,比如說注冊頁面) @Override public void configure(WebSecurity web) throws Exception { web .ignoring()//一般隻需要重寫ignoring()配置 .antMatchers("/static/**"); } }
案例代碼
import com.cjs.sso.service.NbspUserDetailsService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
/**
* @author CharlesYan
* @date 2019-02-11
*/
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private NbspUserDetailsService userDetailsService;
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
}
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/assets/**", "/css/**", "/images/**");
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin()
.loginPage("/login")
.and()
.authorizeRequests()
.antMatchers("/login").permitAll()
.anyRequest()
.authenticated()
.and().csrf().disable().cors();
}
}
用戶端詳細資訊表(官方)
- 建表語句
create table oauth_client_details ( client_id VARCHAR(256) PRIMARY KEY, resource_ids VARCHAR(256), client_secret VARCHAR(256), scope VARCHAR(256), authorized_grant_types VARCHAR(256), web_server_redirect_uri VARCHAR(256), authorities VARCHAR(256), access_token_validity INTEGER, refresh_token_validity INTEGER, additional_information VARCHAR(4096), autoapprove VARCHAR(256) )ENGINE=InnoDB DEFAULT CHARSET=utf8; INSERT INTO oauth_client_details (client_id, client_secret, scope, authorized_grant_types, web_server_redirect_uri, authorities, access_token_validity, refresh_token_validity, additional_information, autoapprove) VALUES ('user-client', '$2a$10$o2l5kA7z.Caekp72h5kU7uqdTDrlamLq.57M1F6ulJln9tRtOJufq', 'all', 'authorization_code,refresh_token,password', null, null, 3600, 36000, null, true); INSERT INTO oauth_client_details (client_id, client_secret, scope, authorized_grant_types, web_server_redirect_uri, authorities, access_token_validity, refresh_token_validity, additional_information, autoapprove) VALUES ('order-client', '$2a$10$GoIOhjqFKVyrabUNcie8d.ADX.qZSxpYbO6YK4L2gsNzlCIxEUDlW', 'all', 'authorization_code,refresh_token,password', null, null, 3600, 36000, null, true);
-
注意事項
client_secret 字段不能直接是 secret 的原始值,需要經過加密。因為是用的 BCryptPasswordEncoder,是以最終插入的值應該是經過 BCryptPasswordEncoder.encode()之後的值。
核心配置檔案
- application.yml
spring: datasource: url: jdbc:mysql://localhost:3306/nbsp?characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai username: root password: 1q2w3e4r # driver-class-name: com.mysql.jdbc.Driver jpa: show-sql: true session: store-type: redis redis: host: 192.168.159.133 password: redispassword port: 6379 server: port: 8080
- 注意事項
- Spring Boot 2.0 之後預設使用 hikari 作為資料庫連接配接池。如果使用其他連接配接池需要引入相關包,然後對應的增加配置。
用戶端配置
依賴配置
- pom.xml
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.1.3.RELEASE</version> <relativePath/> <!-- lookup parent from repository --> </parent> <groupId>com.cjs.sso</groupId> <artifactId>oauth2-sso-client-member</artifactId> <version>0.0.1-SNAPSHOT</version> <name>oauth2-sso-client-member</name> <description>Demo project for Spring Boot</description> <properties> <java.version>1.8</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-oauth2-client</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <dependency> <groupId>org.springframework.security.oauth.boot</groupId> <artifactId>spring-security-oauth2-autoconfigure</artifactId> <version>2.1.3.RELEASE</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency> <dependency> <groupId>org.thymeleaf.extras</groupId> <artifactId>thymeleaf-extras-springsecurity5</artifactId> <version>3.0.4.RELEASE</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>com.h2database</groupId> <artifactId>h2</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-test</artifactId> <scope>test</scope> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>
WebSecurityConfig(自定義安全配置類)
ResourceServerConfig(資源配置類)
- Redis儲存Token
@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()); } }
- JWT儲存Token
@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); } }
介紹
-
相關注解
在 OAuth2 的概念裡,所有的接口都被稱為資源,接口的權限也就是資源的權限,是以 Spring Security OAuth2 中提供了關于資源的注解 @EnableResourceServer 和 @EnableWebSecurity的作用類似
注意事項
- @EnableOAuth2Sso注解
- 引入依賴
<!-- Spring Boot 2.x 中沒有@EnableOAuth2Sso這個注解 -->
<dependency>
<groupId>org.springframework.security.oauth.boot</groupId>
<artifactId>spring-security-oauth2-autoconfigure</artifactId>
<version>2.1.3.RELEASE</version>
</dependency>
- 源碼
單點登入實作之基于OAuth2.0協定OAuth2OAuth2實作單點登入參考連結 -
簡介
對于OAuth 2.0用戶端配置,簡化的配置用@EnableOAuth2Client。這個注解做兩件事情:
(1) 建立一個過濾器(ID是oauth2ClientContextFilter)來存儲目前的請求和上下文。在請求期間需要進行身份認證時,它管理重定向URI。
(2) 在請求範圍内建立一個AccessTokenRequest類型的bean。對于授權代碼(或隐式)授予用戶端是很有用的,可以避免與單個使用者相關的狀态發生沖突。
-
參考連結
https://projects.spring.io/spring-security-oauth/docs/oauth2.html
https://www.cnblogs.com/trust-freedom/p/12002089.html
https://www.jianshu.com/p/fe1194ca8ecd
- 其他
-
antMatcher()
是HttpSecurity的一個方法,他隻告訴了Spring我隻配置了一個我這個Adapter能處理哪個的url,它與authorizeRequests()沒有任何關系。
-
authorizeRequests().antMatchers()
是在antMatchers()中指定的一個或多個路徑,比如執行permitAll()或hasRole()。他們在第一個http.antMatcher()比對時就會生效。
-
案例代碼
- ClientWebsecurityConfigurer類
import org.springframework.boot.autoconfigure.security.oauth2.client.EnableOAuth2Sso; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; @Configuration @EnableWebSecurity @EnableGlobalMethodSecurity(prePostEnabled = true) @EnableOAuth2Sso public class ClientWebsecurityConfigurer extends WebSecurityConfigurerAdapter { @Override public void configure(HttpSecurity http) throws Exception { http.antMatcher("/**").authorizeRequests() .anyRequest().authenticated(); } }
參考連結
-
OAuth2實作單點登入SSO
https://www.cnblogs.com/cjsblog/p/10548022.html
-
Spring Cloud OAuth2 實作使用者認證及單點登入
https://www.cnblogs.com/fengzheng/p/11724625.html
-
OAuth2介紹與使用
https://www.jianshu.com/p/4f5fcddb4106