單點登陸系列(2) - Spring OAuth2項目搭建

概述

在第一篇中,咱們已經講過了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);
    }
}

自定義模塊

一、自定義登陸/註銷

1.一、登陸頁

首先自定義開發一個登陸頁,並開發接口保證訪問url能訪問到登陸頁,例如:/login 。

其次在WebSecurityConfigurerAdapter實現類的configure(HttpSecurity http)方法中,指明自定義登陸頁頁路徑 .formLogin().loginPage("/login")

1.二、註銷

在登陸成功後會生成認證經過的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();
        }
    }
}

二、自定義token

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單點登陸系統。

相關文章
相關標籤/搜索