1. 程式人生 > >Spring Cloud OAuth2(一) 搭建授權服務

Spring Cloud OAuth2(一) 搭建授權服務

tde csr pub 賬號密碼 mailto 默認 HA zab acc

概要

本文內容主要為spring cloud 授權服務的搭建,采用jwt認證。
GitHub 地址:https://github.com/fp2952/spring-cloud-base/tree/master/auth-center/auth-center-provider

添加依賴

Spring Security 及 Security 的OAuth2 擴展

        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-security</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-oauth2</artifactId>
        </dependency>

啟動類註解

啟動類添加 @EnableAuthorizationServer 註解

@SpringCloudApplication
@EnableAuthorizationServer
@EnableFeignClients("com.peng.main.client")
public class AuthCenterProviderApplication {
   public static void main(String[] args){
       SpringApplication.run(AuthCenterProviderApplication.class, args);
   }
}

Oauth2配置類AuthorizationServerConfigurerAdapter

AuthorizationServerConfigurerAdapter中:

  • ClientDetailsServiceConfigurer:用來配置客戶端詳情服務(ClientDetailsService),客戶端詳情信息在這裏進行初始化,你能夠把客戶端詳情信息寫死在這裏或者是通過數據庫來存儲調取詳情信息。
  • AuthorizationServerSecurityConfigurer:用來配置令牌端點(Token Endpoint)的安全約束.
  • AuthorizationServerEndpointsConfigurer:用來配置授權(authorization)以及令牌(token)的訪問端點和令牌服務(token services)。
    主要配置如下:

配置客戶端詳情信息(Client Details)

ClientDetailsServiceConfigurer (AuthorizationServerConfigurer 的一個回調配置項) 能夠使用內存或者JDBC來實現客戶端詳情服務(ClientDetailsService),Spring Security OAuth2的配置方法是編寫@Configuration類繼承AuthorizationServerConfigurerAdapter,然後重寫void configure(ClientDetailsServiceConfigurer clients)方法,如:

    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        // 使用JdbcClientDetailsService客戶端詳情服務
        clients.withClientDetails(new JdbcClientDetailsService(dataSource));
    }

這裏使用Jdbc實現客戶端詳情服務,數據源dataSource不做敘述,使用框架默認的表,schema鏈接:
https://github.com/spring-projects/spring-security-oauth/blob/master/spring-security-oauth2/src/test/resources/schema.sql

配置令牌 管理 (jwtAccessTokenConverter)

JwtAccessTokenConverter是用來生成token的轉換器,而token令牌默認是有簽名的,且資源服務器需要驗證這個簽名。此處的加密及驗簽包括兩種方式:
對稱加密、非對稱加密(公鑰密鑰)
對稱加密需要授權服務器和資源服務器存儲同一key值,而非對稱加密可使用密鑰加密,暴露公鑰給資源服務器驗簽,本文中使用非對稱加密方式,配置於AuthorizationServerConfigurerAdapter如下:

    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
        endpoints.authenticationManager(authenticationManager)
                // 配置JwtAccessToken轉換器
                .accessTokenConverter(jwtAccessTokenConverter())
                // refresh_token需要userDetailsService
                .reuseRefreshTokens(false).userDetailsService(userDetailsService);
                //.tokenStore(getJdbcTokenStore());
    }

    /**
     * 使用非對稱加密算法來對Token進行簽名
     * @return
     */
    @Bean
    public JwtAccessTokenConverter jwtAccessTokenConverter() {

        final JwtAccessTokenConverter converter = new JwtAccessToken();
        // 導入證書
        KeyStoreKeyFactory keyStoreKeyFactory =
                new KeyStoreKeyFactory(new ClassPathResource("keystore.jks"), "mypass".toCharArray());
        converter.setKeyPair(keyStoreKeyFactory.getKeyPair("mytest"));

        return converter;
    }

通過 JDK 工具生成 JKS 證書文件,並將 keystore.jks 放入resource目錄下
keytool -genkeypair -alias mytest -keyalg RSA -keypass mypass -keystore keystore.jks -storepass mypass

此處我們自定義JwtAccessToken用於添加額外用戶信息

/**
 * Created by fp295 on 2018/4/16.
 * 自定義JwtAccessToken轉換器
 */
public class JwtAccessToken extends JwtAccessTokenConverter {

    /**
     * 生成token
     * @param accessToken
     * @param authentication
     * @return
     */
    @Override
    public OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication) {
        DefaultOAuth2AccessToken defaultOAuth2AccessToken = new DefaultOAuth2AccessToken(accessToken);

        // 設置額外用戶信息
        BaseUser baseUser = ((BaseUserDetail) authentication.getPrincipal()).getBaseUser();
        baseUser.setPassword(null);
        // 將用戶信息添加到token額外信息中
        defaultOAuth2AccessToken.getAdditionalInformation().put(Constant.USER_INFO, baseUser);

        return super.enhance(defaultOAuth2AccessToken, authentication);
    }

    /**
     * 解析token
     * @param value
     * @param map
     * @return
     */
    @Override
    public OAuth2AccessToken extractAccessToken(String value, Map<String, ?> map){
        OAuth2AccessToken oauth2AccessToken = super.extractAccessToken(value, map);
        convertData(oauth2AccessToken, oauth2AccessToken.getAdditionalInformation());
        return oauth2AccessToken;
    }

    private void convertData(OAuth2AccessToken accessToken,  Map<String, ?> map) {
        accessToken.getAdditionalInformation().put(Constant.USER_INFO,convertUserData(map.get(Constant.USER_INFO)));

    }

    private BaseUser convertUserData(Object map) {
        String json = JsonUtils.deserializer(map);
        BaseUser user = JsonUtils.serializable(json, BaseUser.class);
        return user;
    }
}

JwtAccessToken 類中從authentication裏的getPrincipal(實際為UserDetails接口)獲取用戶信息,所以我們需要實現自己的UserDetails

/**
 * Created by fp295 on 2018/4/29.
 * 包裝org.springframework.security.core.userdetails.User類
 */
public class BaseUserDetail implements UserDetails, CredentialsContainer {

    private final BaseUser baseUser;
    private final org.springframework.security.core.userdetails.User user;

    public BaseUserDetail(BaseUser baseUser, User user) {
        this.baseUser = baseUser;
        this.user = user;
    }


    @Override
    public void eraseCredentials() {
        user.eraseCredentials();
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return user.getAuthorities();
    }

    @Override
    public String getPassword() {
        return user.getPassword();
    }

    @Override
    public String getUsername() {
        return user.getUsername();
    }

    @Override
    public boolean isAccountNonExpired() {
        return user.isAccountNonExpired();
    }

    @Override
    public boolean isAccountNonLocked() {
        return user.isAccountNonLocked();
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return user.isCredentialsNonExpired();
    }

    @Override
    public boolean isEnabled() {
        return user.isEnabled();
    }

    public BaseUser getBaseUser() {
        return baseUser;
    }
}

授權端點開放

    @Override
    public void configure(AuthorizationServerSecurityConfigurer oauthServer) {
        oauthServer
                // 開啟/oauth/token_key驗證端口無權限訪問
                .tokenKeyAccess("permitAll()")
                // 開啟/oauth/check_token驗證端口認證權限訪問
                .checkTokenAccess("isAuthenticated()");
    }

Security 配置

需要配置 DaoAuthenticationProvider、UserDetailService 等

@Configuration
@Order(ManagementServerProperties.ACCESS_OVERRIDE_ORDER)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    // 自動註入UserDetailsService
    @Autowired
    private BaseUserDetailService baseUserDetailService;

    @Override
    public void configure(HttpSecurity http) throws Exception {
        http    // 配置登陸頁/login並允許訪問
                .formLogin().permitAll()
                // 登出頁
                .and().logout().logoutUrl("/logout").logoutSuccessUrl("/")
                // 其余所有請求全部需要鑒權認證
                .and().authorizeRequests().anyRequest().authenticated()
                // 由於使用的是JWT,我們這裏不需要csrf
                .and().csrf().disable();
    }

    /**
     * 用戶驗證
     * @param auth
     */
    @Override
    public void configure(AuthenticationManagerBuilder auth) {
        auth.authenticationProvider(daoAuthenticationProvider());
    }


    @Bean
    public DaoAuthenticationProvider daoAuthenticationProvider(){
        DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
        // 設置userDetailsService
        provider.setUserDetailsService(baseUserDetailService);
        // 禁止隱藏用戶未找到異常
        provider.setHideUserNotFoundExceptions(false);
        // 使用BCrypt進行密碼的hash
        provider.setPasswordEncoder(new BCryptPasswordEncoder(6));
        return provider;
    }
}

UserDetailsService 實現

@Service
public class BaseUserDetailService implements UserDetailsService {

    private Logger logger = LoggerFactory.getLogger(this.getClass());

    @Autowired
    private BaseUserService baseUserService;
    @Autowired
    private BaseRoleService baseRoleService;
 
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {

        // 調用FeignClient查詢用戶
        ResponseData<BaseUser> baseUserResponseData = baseUserService.getUserByUserName(username);
        if(baseUserResponseData.getData() == null || !ResponseCode.SUCCESS.getCode().equals(baseUserResponseData.getCode())){
            logger.error("找不到該用戶,用戶名:" + username);
            throw new UsernameNotFoundException("找不到該用戶,用戶名:" + username);
        }
        BaseUser baseUser = baseUserResponseData.getData();

        // 調用FeignClient查詢角色
        ResponseData<List<BaseRole>> baseRoleListResponseData = baseRoleService.getRoleByUserId(baseUser.getId());
        List<BaseRole> roles;
        if(baseRoleListResponseData.getData() == null ||  !ResponseCode.SUCCESS.getCode().equals(baseRoleListResponseData.getCode())){
            logger.error("查詢角色失敗!");
            roles = new ArrayList<>();
        }else {
            roles = baseRoleListResponseData.getData();
        }

        // 獲取用戶權限列表
        List<GrantedAuthority> authorities = new ArrayList();
        roles.forEach(e -> {
            // 存儲用戶、角色信息到GrantedAuthority,並放到GrantedAuthority列表
            GrantedAuthority authority = new SimpleGrantedAuthority(e.getRoleCode());
            authorities.add(authority);
        
        });

        // 返回帶有用戶權限信息的User
        org.springframework.security.core.userdetails.User user =  new org.springframework.security.core.userdetails.User(baseUser.getUserName(),
                baseUser.getPassword(), isActive(baseUser.getActive()), true, true, true, authorities);
        return new BaseUserDetail(baseUser, user);
    }

    private boolean isActive(int active){
        return active == 1 ? true : false;
    }
}

授權服務器驗證

http://127.0.0.1:8080/oauth/authorize?client_id=clientId&response_type=code&redirect_uri=www.baidu.com

註意:client_id:為存儲在數據庫裏的client_id, response_type:寫死code

  1. 鏈接回車後進入spring security 的簡單登陸頁面,

技術分享圖片

  1. 輸入賬號密碼,為實現的 UserDetailsService 要裏獲取的用戶,點擊 login,
  2. 進入簡單授權頁面,點擊 Authorize,
  3. 重定向到 redirect_uri,並帶有 code 參數:
    http://www.baidu.com?code=rTKETX
  4. post請求獲取 token:
    技術分享圖片

註意,此處需加 Authorization 請求頭,值為 Basic xxx xxx 為 client_id:client_secret 的 base64編碼,返回:

Spring Cloud OAuth2(一) 搭建授權服務