在第一篇中,咱們已經講過了OAuth2單點登陸的實際應用場景和技術方案,那麼這一篇就具體講解如何搭建OAuth2的服務。OAuth2只是一個協議,實現該協議的技術產品有不少,好比:微軟的ADFS,Oracle的OAM(12c),等等。但這些產品都是大廠研發出來的,基本都是收費的,那麼若是咱們須要基於開源的技術,本身搭建基於OAuth2的服務該怎麼作呢?你能夠試試「Spring Cloud全家桶」裏面的Spring Security OAuth2。前端
本文將講解Spring Security OAuth2的項目實戰搭建,因爲篇幅有限,文章中只會摘錄核心代碼,完整代碼請上 github地址 查看。java
最近看過一個很是複雜Spring Security OAuth2技術架構圖,雖然不少功能點我本身也沒有用到過,可是這裏仍是附上吧。git
首先是建立一個SpringBoot項目,要在啓動類加上 @EnableResourceServer 的註解。github
pom.xmlweb
<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.0.4.RELEASE</version> <relativePath/> <!-- lookup parent from repository --> </parent> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!--oauth2--> <dependency> <groupId>org.springframework.security.oauth</groupId> <artifactId>spring-security-oauth2</artifactId> <version>2.0.15.RELEASE</version> </dependency> <!--freemarker,自定義登陸頁使用--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-freemarker</artifactId> </dependency> <!--jwt,生成jwt token--> <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-jwt</artifactId> <version>1.0.9.RELEASE</version> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.6.0</version> </dependency> <!-- feign,非必需 --> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-openfeign</artifactId> <version>2.0.2.RELEASE</version> </dependency> </dependencies>
核心的實現類主要只有兩個,一個實現接口 WebSecurityConfigurerAdapter,另外一個實現AuthorizationServerConfigurerAdapter接口。spring
WebSecurityConfigurerAdapter主要用來定義Web請求的路由控制,好比:哪些路由受security控制;自定義登陸頁;登陸成功或失敗的處理;註銷的處理,等等。包括還有 web.ignoring() 的方法,能夠對指定url路徑放行,不受單點登陸控制。數據庫
WebSecurityCA.javajson
@Configuration @Order(1) public class WebSecurityCA extends WebSecurityConfigurerAdapter { @Autowired private AuthenticationFailureHandler appLoginFailureHandler; @Override protected void configure(HttpSecurity http) throws Exception { http.requestMatchers() .antMatchers("/login") .antMatchers("/oauth/authorize") .antMatchers("/oauth/token") .antMatchers("/logout") .and() .authorizeRequests() .anyRequest().authenticated() .and() // 自定義登陸頁面,這裏配置了 loginPage, 就會經過 LoginController 的 login 接口加載登陸頁面 .formLogin() .loginPage("/login") .permitAll() .failureHandler(appLoginFailureHandler) .failureUrl("/login?error=true") //註銷 .and() .logout() .addLogoutHandler(new MyLogoutHandler()) .and() .csrf().disable(); } /** * web ignore比較適合配置前端相關的靜態資源,它是徹底繞過spring security的全部filter的 * ingore是徹底繞過了spring security的全部filter,至關於不走spring security * permitall沒有繞過spring security,其中包含了登陸的以及匿名的 * * @param web * @throws Exception */ @Override public void configure(WebSecurity web) throws Exception { web.ignoring().antMatchers("/oauth/logout"); } /** * 建立該實例,爲了保證 密碼模式中能夠實現AuthenticationManager * * @return * @throws Exception */ @Bean(name = BeanIds.AUTHENTICATION_MANAGER) @Override public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); } }
AuthorizationServerConfigurerAdapter接口則是實現OAuth2的核心代碼,實現功能包括:開放OAuth2的驗證模式;開放的clientId和clientSecret;token按照jwt協議生成;等等。cookie
AuthorizationServerCA.java架構
@Configuration @EnableAuthorizationServer public class AuthorizationServerCA extends AuthorizationServerConfigurerAdapter { @Autowired private BCryptPasswordEncoder passwordEncoder; @Autowired private AuthenticationManager authenticationManager; @Autowired private UserDetailsService userDetailsService; @Autowired private TokenStore tokenStore; @Autowired(required = false) private JwtAccessTokenConverter jwtAccessTokenConverter; @Autowired(required = false) private TokenEnhancer jwtTokenEnhancer; @Autowired private WebResponseExceptionTranslator customWebResponseExceptionTranslator; @Autowired private OAuth2Properties oAuth2Properties; @Override public void configure(final AuthorizationServerSecurityConfigurer oauthServer) throws Exception { oauthServer.tokenKeyAccess("permitAll()").checkTokenAccess("isAuthenticated()"); } @Override public void configure(final ClientDetailsServiceConfigurer clients) throws Exception { InMemoryClientDetailsServiceBuilder build = clients.inMemory(); for (OAuth2ClientsProperties config : oAuth2Properties.getClients()) { build.withClient(config.getClientId()) .secret(passwordEncoder.encode(config.getClientSecret())) .accessTokenValiditySeconds(config.getAccessTokenValiditySeconds()) .authorizedGrantTypes("refresh_token", "password", "authorization_code")//OAuth2支持的驗證模式 .scopes("user_info") .autoApprove(true); } } /** * 密碼password模式,須要實現該方法 authenticationManager * @param endpoints * @throws Exception */ @Override public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception { endpoints.tokenStore(tokenStore) .authenticationManager(authenticationManager) .userDetailsService(userDetailsService); //擴展token返回結果 if (jwtAccessTokenConverter != null && jwtTokenEnhancer != null) { TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain(); List<TokenEnhancer> enhancerList = new ArrayList(); enhancerList.add(jwtTokenEnhancer); enhancerList.add(jwtAccessTokenConverter); tokenEnhancerChain.setTokenEnhancers(enhancerList); //jwt endpoints.tokenEnhancer(tokenEnhancerChain) .accessTokenConverter(jwtAccessTokenConverter); } endpoints.exceptionTranslator(customWebResponseExceptionTranslator); } }
首先自定義開發一個登陸頁,並開發接口保證訪問url能訪問到登陸頁,例如:/login 。
其次在WebSecurityConfigurerAdapter實現類的configure(HttpSecurity http)方法中,指明自定義登陸頁頁路徑 .formLogin().loginPage("/login")
在登陸成功後會生成認證經過的cookie,保證下次跳轉到登陸頁時無需登陸就能經過。而註銷的操做就是清除該cookie,Spring OAuth2默認的註銷地址是:/logout,而且註銷成功後會自動重定向到登陸頁。
修改方式一樣也是在WebSecurityConfigurerAdapter實現類的configure(HttpSecurity http)方法中,.logout().addLogoutHandler(new MyLogoutHandler())方法能夠自定義註銷的實現邏輯,例如MyLogoutHandler()就是我本身實現的處理邏輯,註銷成功後會跳轉到上一頁。
MyLogoutHandler.java
@Component public class MyLogoutHandler implements LogoutHandler { @Override public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) { try { final String refererUrl = request.getHeader("Referer"); response.sendRedirect(refererUrl);//實現自定義重定向 } catch (IOException e) { e.printStackTrace(); } } }
Spring OAuth2 在登陸成功後會生成access_token和refresh_token,但這些token默認是相似於uuid的字符串,咱們怎麼把他們換成 jwt的token呢?
在以前AuthorizationServerCA.java 類中咱們能看到使用jwt方式發放token的配置,包括其中有用到自定義的JwtTokenEnhancer類,能夠經過.setAdditionalInformation拓展更多的自定義參數。
public class JwtTokenEnhancer implements TokenEnhancer { @Override public OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication) { Map<String, Object> info = new HashMap<>(); info.put("name","吳晨瑞"); ((DefaultOAuth2AccessToken) accessToken).setAdditionalInformation(info); return accessToken; } }
在用戶輸入用戶名和密碼後,校驗是否正常的程序在哪裏定義呢?咱們通常要自定義類來實現UserDetailsService接口。這個接口裏面只有一個方法 loadUserByUsername(String username),傳入參數是 用戶名,你能夠自定義方法獲取數據庫中該用戶名對應的密碼,而後Spring Auth2服務會將你數據庫中獲取的密碼和頁面上輸入的密碼比對,判斷你是否登陸成功。
MyUserDetailsService.java
@Component public class MyUserDetailsService implements UserDetailsService { @Resource private UserFeign userFeign; @Override public UserDetails loadUserByUsername(String username) throws BadCredentialsException { //enable :用戶已失效 //accountNonExpired:用戶賬號已過時 //credentialsNonExpired:壞的憑證 //accountNonLocked:用戶帳號已鎖定 // return new User("dd", "1", true, true, false, true, AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_USER")); String password= userFeign.loadUserByUsername(username); return new User(username, passwordEncoder().encode(password), AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_USER")); } @Bean public BCryptPasswordEncoder passwordEncoder(){ return new BCryptPasswordEncoder(); } }
還有一些自定義的登陸異常處理、權限異常處理,這裏就不一一附上了,能夠在github上參考相關代碼。Spring OAuth2 有本身一套很是完整的體系,各個接口均可以自定義實現,就像文章開頭我附上的那張圖同樣。若是各位看客感興趣而且有時間,能夠一一實習這些接口,打造一個本身OAuth2單點登陸系統。