Spring Boot - Spring Security

  • Spring MVC的安全認證
    • 先建立一個繼承自WebSecurityConfigurerAdapter的配置類,標識爲@Configuration和@EnableWebSecurity註解,進行Spring MVC安全驗證設置,默認不作什麼自定義配置,便可以在訪問接口等資源時返回一個登錄頁讓輸入帳號密碼(啓動時會生成並在日誌中輸出一個帳號密碼),也能夠本身設置頁面和帳號密碼。
  • Restful API的安全認證
    • 一般在先後端分離的狀況下,會有一個單獨的登錄API甚至登錄Server如Open Auth Server或OpenID Server,因爲驗證帳號密碼和返回token,而後在訪問業務API時帶上token和請求參數
      • 先建立一個繼承自WebSecurityConfigurerAdapter的配置類,標識爲@Configuration、@EnableWebSecurity和@EnableGlobalMethodSecurity註解,進行基於Method的Restful API安全驗證設置
        • @EnableGlobalMethodSecurity的prePostEnabled參數用於控制@PreAuthorize、@PostAuthorize、@PreFilter、@PostFilter註解是否生效,前兩個可使用表達式判斷用戶是否有某些角色或受權
        • @EnableGlobalMethodSecurity的securedEnabled參數用於控制@Secured註解是否生效
        • @EnableGlobalMethodSecurity的jsr250Enabled參數用於控制@RolesRequired註解是否生效
      • 再建立一個獲取token的登錄、登出的API,如POST /token
      • 最後建立一個自定義的Filter如AuthenticationTokenFilter,繼承自OncePerRequestFilter,重寫doFilterInternal方法,從request的頭部獲取"Authorization"和"Bearer "字段),而後進行基於token的身份驗證(即根據token獲取用戶信息),最後把用戶認證信息(如UsernamePasswordAuthenticationToken)放到SpringContextHolder中。
  • OAuth2.0認證和OpenId
    • OpenId比較老,只實現了用戶的追蹤,新版也有了認證,但沒有受權的功能,目前微信這種應用的第三方認證只是借用了OpenId的概念,並非一回事
    • OAuth
      • 目前是2.0,與1.0不兼容
      • 密碼不須要告訴第三方應用
      • 支持四種受權模式,受權碼模式(Authorization Code)最經常使用,密碼模式(Password)一些場景也有使用,其餘兩種簡化模式(Implicit)和客戶端模式(Client Credentials)應用較少。
      • Refresh Token不是一種受權模式,可是Spring中能夠用於刷新token
      • 流程
        • 能夠用postman模擬?
        • 受權碼模式
          • 傳統方式
            • OAuth認證完成後經過callback跳回原服務器地址,並同時帶上受權碼code參數
            • token只存在於服務器和OAuth服務器間
            • 客戶端只知道受權碼,服務器用這個code去OAuth服務器換取token,而後服務器再去OAuth服務器端用token換取用戶信息
          • JWT
            • 自認證的token,使用其中第三部分的簽名驗證token的有效性(一般用SHA256公私鑰不對稱加密)
            • 經常使用於API訪問
            • 那麼服務器和OAuth服務器的兩次交互就變成了一次,直接用token換取用戶信息
            • 客戶端也直接使用這個自認證的token,而不是受權碼code了
            • 載荷中只作了base64,不要放敏感信息
    • Spring Boot中的OAuth認證明現
      • 主要是要用平臺公鑰解析請求中的Bearer等token,獲取有用信息
      • 在一個@EnableResourceServer標註的config類中配置當前app須要使用OAuth功能(解析token進行認證等)
        • 重寫public void configure(final ResourceServerSecurityConfigurer resources)、public void configure(final HttpSecurity http) throws Exception方法,其中使用本身後面實現的類來解析和覆蓋設置token信息、用actuator.endpoints.scope控制/manage/的訪問?
        • 也能夠把這個config和其餘config一塊兒作成註解,給其餘app使用
      • 實現一個本身的類,用於解析token(由於有時須要使用本身公司或者平臺的特殊公鑰來進行解密,這個公鑰說不定是配在環境變量中的,因此須要一些特殊處理)
/**
 * Annotation of enable oauth 2 security support.
 */
@Retention(value = RetentionPolicy.RUNTIME)
@Target(value = ElementType.TYPE)
@Documented
@Import({OAuth2ResourceServerConfiguration.class, WebSecurityConfig.class, OAuth2SecurityConfig.class})
public @interface EnableOAuthSecurity
{
}




/**
 * Configuration of Spring Security OAuth2 for resource server.
 */
@EnableResourceServer
@Configuration
@PropertySource("classpath:actuator-security.properties")
public class OAuth2ResourceServerConfiguration extends ResourceServerConfigurerAdapter
{
    @Autowired
    private ResourceServerTokenServices tokenServices;

    @Value("${uaa.resourceId:#{null}}")
    private String resourceId;

    @Value("#{'${actuator.secured.endpoints}'.split(',')}")
    private List<String> securedActuatorEndpoints;

    @Value("${actuator.endpoints.scope}")
    private String scope;

    @Override
    public void configure(final ResourceServerSecurityConfigurer resources)
    {
        resources.resourceId(resourceId);
        resources.tokenServices(tokenServices);
    }

    @Override
    public void configure(final HttpSecurity http) throws Exception
    {
        for (String securedActuatorEndpoint : securedActuatorEndpoints)
        {
            http.authorizeRequests()
                .antMatchers("/manage/" + securedActuatorEndpoint)
                .access("#oauth2.hasScope('" + scope + "')");
        }
        http.authorizeRequests().anyRequest().permitAll();
    }
}


public interface ResourceServerTokenServices {

    /**
     * Load the credentials for the specified access token.
     *
     * @param accessToken The access token value.
     * @return The authentication for the access token.
     * @throws AuthenticationException If the access token is expired
     * @throws InvalidTokenException if the token isn't valid
     */
    OAuth2Authentication loadAuthentication(String accessToken) throws AuthenticationException, InvalidTokenException;

    /**
     * Retrieve the full access token details from just the value.
     * 
     * @param accessToken the token value
     * @return the full access token with client id etc.
     */
    OAuth2AccessToken readAccessToken(String accessToken);

}


/**
 * Handles incoming JWT access tokens issued by UAA server.
 */
@Component
public class RestControllerTokenServices implements ResourceServerTokenServices {
    public static final String ZID = "zid";
    public static final String TEN = "ten";
    public static final String ISS = "iss";
    public static final String SCOPE = "scope";
    public static final String USER_NAME = "user_name";
    public static final String EMAIL = "email";

    public static final String TENANT_NAME_IN_SUBDOMAIN = "tenantNameInSubdomain";
    public static final String TENANT_ID = "tenantId";

    private static final Logger LOG = LoggerFactory.getLogger(RestControllerTokenServices.class);

    private JwtTokenStore tokenStore;

    private List<UaaPublicKeyResource> keyList = new ArrayList<UaaPublicKeyResource>();

    @Autowired
    private OAuth2ConfigurationService config;

    @Autowired
    private PublicKeyProvider publicKeyProvider;

    @PostConstruct
    public void initialize() throws Exception {
        LOG.info(String.format("TokenServices constructed with configuration %s", config));

        keyList = publicKeyProvider.getPublicKeysFromUaa();
    }

    /**
     * Checks token and extracts authentication and further information.
     *
     * The token validity is checked with a public signing key obtained from the
     * UAA server. Further token information is extracted and stored in the
     * SecurityContext to be retrievable in the application.
     */
    @Override
    public OAuth2Authentication loadAuthentication(final String accessToken) throws InvalidTokenException
    {
        try {
            setVerifierKey(accessToken);
        } catch (Exception e) {
            LOG.error("Error while setting verifier key", e);
        }
        final OAuth2AccessToken token = tokenStore.readAccessToken(accessToken);  
        final OAuth2Authentication auth = tokenStore.readAuthentication(token);
        String tenant = extractTenantIdFromToken(token);
        
        String tenantSubdomain = extractTenantNameInSubdomainFromToken(token);
        auth.setDetails(ImmutableMap.of(TENANT_ID, tenant, TENANT_NAME_IN_SUBDOMAIN, tenantSubdomain));

        LOG.debug("Loaded authentication token for tenant '{}'", tenant);       
        return auth;
    }

    private void setVerifierKey(String token) throws Exception  {
        LOG.info("STEPIN setVerifierKey");

        String publicKeyForXXX = null;
        JwtTokenHeader jwtHeader = JwtTools.extractHeaderFromJwt(token);
        for (int i = 0; i < keyList.size(); i++) {
            if (jwtHeader.getKeyId().equals(keyList.get(i).getKid())) {
                publicKeyForXXX = keyList.get(i).getValue();
                LOG.debug("getKid():" + keyList.get(i).getKid());
                LOG.debug("Public key for current token:" + publicKeyForXXX);
            }
        }
        if(publicKeyForXXX == null)
        {
            publicKeyForXXX = PublicKeyProvider.getDefaultPublicKey();
            LOG.info("Public key was set to default");
        }
        
        // Construct token decoding objects using the signing key
        final JwtAccessTokenConverter tokenConverter = new JwtAccessTokenConverter() {
            @Override
            protected Map<String, Object> decode(final String token) {
                Map<String, Object> decodedToken = super.decode(token);
                decodedToken.put("authorities", decodedToken.get(SCOPE));
                return decodedToken;
            }
        };

        tokenConverter.setVerifierKey(publicKeyForXXX);
        tokenConverter.afterPropertiesSet();
        tokenStore = new JwtTokenStore(tokenConverter);
        LOG.info("STEPOUT setVerifierKey");
    }

    @Override
    public OAuth2AccessToken readAccessToken(final String accessToken) {
        return tokenStore.readAccessToken(accessToken);
    }

    private String extractTenantIdFromToken(final OAuth2AccessToken token) {
        return (String) token.getAdditionalInformation().get(TEN);
    }

    private String extractTenantNameInSubdomainFromToken(final OAuth2AccessToken token) {
        final String uaaUrl = (String) token.getAdditionalInformation().get(ISS);
        return uaaUrl.split("[/-]")[2];
    }
    

}
相關文章
相關標籤/搜索