>  기사  >  Java  >  Spring Security 원칙 소개(코드 포함)

Spring Security 원칙 소개(코드 포함)

不言
不言앞으로
2019-03-22 16:49:413371검색

이 기사는 Spring Security의 원칙(코드 포함)을 소개합니다. 도움이 필요한 친구들이 참고할 수 있기를 바랍니다.

적과 자신을 알아야 모든 전투에서 승리할 수 있습니다. Spring Security를 ​​사용하여 원하는 대로 확장할 수 있도록 그 원리를 이해하는 것이 가장 좋습니다. .

Filter

Spring Security는 기본적으로 필터를 사용하여 구성된 신원 인증, 권한 인증 및 로그아웃을 완료합니다.

Spring Security는 Servlet의 필터 체인에 FilterChainProxy 필터를 등록합니다. 이는 Spring Security 자체에서 유지 관리하는 여러 필터 체인에 대한 요청을 프록시로 지정하고 해당 필터와 일치하면 실행됩니다. 필터 체인은 순차적이며 첫 번째로 일치하는 필터 체인만 요청에 대해 실행됩니다. Spring Security의 구성은 기본적으로 필터를 추가, 삭제, 수정하는 것입니다.

Spring Security 원칙 소개(코드 포함)

기본적으로 시스템에서 삽입한 15개의 필터는 다양한 구성 요구 사항에 해당합니다. 다음으로 UsernamePasswordAuthenticationFilter 필터를 중점적으로 분석합니다. 이는 사용자 이름과 비밀번호를 사용한 로그인 인증에 사용되는 필터입니다. 그러나 많은 경우 우리의 로그인은 단순한 사용자 이름과 비밀번호가 아니라 타사 인증 로그인을 사용할 수도 있습니다. , 이때는 커스텀 필터를 사용해야 합니다. 물론 여기서는 커스텀 필터를 주입하는 방법에 대해서만 자세히 설명하지 않겠습니다.

@Override
protected void configure(HttpSecurity http) throws Exception {
    
    http.addFilterAfter(...);
    ...
 }

신원 인증 프로세스

신원 인증 프로세스를 시작하기 전에 몇 가지 기본 개념을 이해해야 합니다.

  1. SecurityContextHolder

SecurityContextHolder는 SecurityContext 개체를 저장합니다. SecurityContextHolder는 세 가지 저장 모드를 갖춘 저장 에이전트입니다.

  • MODE_THREADLOCAL: SecurityContext는 스레드에 저장됩니다.
  • MODE_INHERITABLETHREADLOCAL: SecurityContext는 스레드에 저장되지만 하위 스레드는 상위 스레드에서 SecurityContext를 얻을 수 있습니다.
  • MODE_GLOBAL: SecurityContext는 모든 스레드에서 동일합니다.

SecurityContextHolder는 기본적으로 MODE_THREADLOCAL 모드를 사용하며 SecurityContext는 현재 스레드에 저장됩니다. SecurityContextHolder를 호출할 때 명시적인 매개변수를 전달할 필요가 없습니다. SecurityContextHolder 개체는 현재 스레드에서 직접 얻을 수 있습니다.

SecurityContext context = SecurityContextHolder.getContext();
Authentication authentication = context.getAuthentication();

2.Authentication

인증은 현재 사용자가 누구인지 나타내는 확인입니다. 검증이란 무엇입니까? 예를 들어 사용자 이름과 비밀번호 세트가 검증입니다. 물론 잘못된 사용자 이름과 비밀번호도 검증이지만 Spring Security는 검증에 실패합니다.

인증 인터페이스

public interface Authentication extends Principal, Serializable {
       //获取用户权限,一般情况下获取到的是用户的角色信息
       Collection extends GrantedAuthority> getAuthorities();
       //获取证明用户认证的信息,通常情况下获取到的是密码等信息,不过登录成功就会被移除
       Object getCredentials();
       //获取用户的额外信息,比如 IP 地址、经纬度等
       Object getDetails();
       //获取用户身份信息,在未认证的情况下获取到的是用户名,在已认证的情况下获取到的是 UserDetails (暂时理解为,当前应用用户对象的扩展)
       Object getPrincipal();
       //获取当前 Authentication 是否已认证
       boolean isAuthenticated();
       //设置当前 Authentication 是否已认证
       void setAuthenticated(boolean isAuthenticated);
}

3.AuthenticationManager ProviderManager AuthenticationProvider

사실 이 세 가지는 쉽게 구별할 수 있습니다. ProviderManager는 주로 AuthenticationManager 인터페이스의 특정 구현 클래스입니다. ProviderManager에 기록된 AuthenticationProvider 개체의 AuthenticationProvider 인터페이스 클래스에는 두 가지 메서드가 있습니다

public interface AuthenticationProvider {
    //实现具体的身份认证逻辑,认证失败抛出对应的异常
    Authentication authenticate(Authentication authentication)
            throws AuthenticationException;
    //该认证类是否支持该 Authentication 的认证
    boolean supports(Class> authentication);
}

다음 단계는 ProviderManager의 공급자 컬렉션을 탐색하여 적절한 AuthenticationProvider를 사용하여 ID 인증을 완료하는 것입니다.

4.UserDetailsService UserDetails

UserDetailsService 인터페이스에는 간단한 메소드가 하나뿐입니다

public interface UserDetailsService {
    //根据用户名查到对应的 UserDetails 对象
    UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}

5.Process

위 개념이 이해가 안 되시면 다음 프로세스에서 천천히 분석해보겠습니다

UsernamePasswordAuthenticationFilter 필터에 먼저 상위 클래스 AbstractAuthenticationProcessingFilter

public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
        throws IOException, ServletException {
    ...
    //首先配对是不是配置的身份认证的URI,是则执行下面的认证,不是则跳过
    if (!requiresAuthentication(request, response)) {
        chain.doFilter(request, response);

        return;
    }
    ...
    Authentication authResult;

    try {
        //关键方法, 实现认证逻辑并返回 Authentication, 由其子类 UsernamePasswordAuthenticationFilter 实现, 由下面 5.3 详解
        authResult = attemptAuthentication(request, response);
        if (authResult == null) {
            // return immediately as subclass has indicated that it hasn't completed
            // authentication
            return;
        }
        sessionStrategy.onAuthentication(authResult, request, response);
    }
    catch (InternalAuthenticationServiceException failed) {
        //认证失败调用...由下面 5.1 详解
        unsuccessfulAuthentication(request, response, failed);

        return;
    }
    catch (AuthenticationException failed) {
        //认证失败调用...由下面 5.1 详解
        unsuccessfulAuthentication(request, response, failed);

        return;
    }

    // Authentication success
    if (continueChainBeforeSuccessfulAuthentication) {
        chain.doFilter(request, response);
    }
    //认证成功调用...由下面 5.2 详解
    successfulAuthentication(request, response, chain, authResult);
}

5.1 인증 실패 처리 논리

protected void unsuccessfulAuthentication(HttpServletRequest request,
                                          HttpServletResponse response, AuthenticationException failed)
        throws IOException, ServletException {
    SecurityContextHolder.clearContext();
    ...
    rememberMeServices.loginFail(request, response);
    //该 handler 处理失败界面跳转和响应逻辑
    failureHandler.onAuthenticationFailure(request, response, failed);
}

의 doFilter() 메서드를 입력하세요. 여기서 기본으로 구성된 실패 처리 핸들러는 사용자 정의할 수 있는 SimpleUrlAuthenticationFailureHandler입니다.

public class SimpleUrlAuthenticationFailureHandler implements
        AuthenticationFailureHandler {
    ...

    public void onAuthenticationFailure(HttpServletRequest request,
            HttpServletResponse response, AuthenticationException exception)
            throws IOException, ServletException {
        //没有配置失败跳转的URL则直接响应错误
        if (defaultFailureUrl == null) {
            logger.debug("No failure URL set, sending 401 Unauthorized error");

            response.sendError(HttpStatus.UNAUTHORIZED.value(),
                HttpStatus.UNAUTHORIZED.getReasonPhrase());
        }
        else {
            //否则
            //缓存异常
            saveException(request, exception);
            //根据配置的异常页面是重定向还是转发进行不同方式跳转
            if (forwardToDestination) {
                logger.debug("Forwarding to " + defaultFailureUrl);

                request.getRequestDispatcher(defaultFailureUrl)
                        .forward(request, response);
            }
            else {
                logger.debug("Redirecting to " + defaultFailureUrl);
                redirectStrategy.sendRedirect(request, response, defaultFailureUrl);
            }
        }
    }
    //缓存异常,转发则保存在request里面,重定向则保存在session里面
    protected final void saveException(HttpServletRequest request,
            AuthenticationException exception) {
        if (forwardToDestination) {
            request.setAttribute(WebAttributes.AUTHENTICATION_EXCEPTION, exception);
        }
        else {
            HttpSession session = request.getSession(false);

            if (session != null || allowSessionCreation) {
                request.getSession().setAttribute(WebAttributes.AUTHENTICATION_EXCEPTION,
                        exception);
            }
        }
    }
}

다음은 작은 확장입니다. 시스템의 오류 처리 핸들러를 사용하여 인증이 실패할 경우 이동할 URL을 지정합니다. MVC의 해당 URL 메서드에서는 키와 피드를 통해 요청이나 세션에서 오류 정보를 얻을 수 있습니다. 5.2 인증 성공 처리 로직

protected void successfulAuthentication(HttpServletRequest request,
                                        HttpServletResponse response, FilterChain chain, Authentication authResult)
        throws IOException, ServletException {
    ...
    //这里要注意很重要,将认证完成返回的 Authentication 保存到线程对应的 `SecurityContext` 中
    SecurityContextHolder.getContext().setAuthentication(authResult);

    rememberMeServices.loginSuccess(request, response, authResult);

    // Fire event
    if (this.eventPublisher != null) {
        eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(
                authResult, this.getClass()));
    }
    //该 handler 就是为了完成页面跳转
    successHandler.onAuthenticationSuccess(request, response, authResult);
}

여기에 구성된 기본 성공 처리 핸들러는 SavedRequestAwareAuthenticationSuccessHandler입니다. 어쨌든 내부 코드는 자세히 확장되지 않으며, 이는 인증 성공 후 지정된 인터페이스로 점프합니다. 사용자 정의할 수 있습니다.

5.3 신원 인증 내역

public class UsernamePasswordAuthenticationFilter extends
        AbstractAuthenticationProcessingFilter {
    ...
    public static final String SPRING_SECURITY_FORM_USERNAME_KEY = "username";
    public static final String SPRING_SECURITY_FORM_PASSWORD_KEY = "password";

    private String usernameParameter = SPRING_SECURITY_FORM_USERNAME_KEY;
    private String passwordParameter = SPRING_SECURITY_FORM_PASSWORD_KEY;
    private boolean postOnly = true;

    ...
    //开始身份认证逻辑
    public Authentication attemptAuthentication(HttpServletRequest request,
            HttpServletResponse response) throws AuthenticationException {
        if (postOnly && !request.getMethod().equals("POST")) {
            throw new AuthenticationServiceException(
                    "Authentication method not supported: " + request.getMethod());
        }

        String username = obtainUsername(request);
        String password = obtainPassword(request);

        if (username == null) {
            username = "";
        }

        if (password == null) {
            password = "";
        }

        username = username.trim();
        //先用前端提交过来的 username 和 password 封装一个简易的 AuthenticationToken
        UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
                username, password);

        // Allow subclasses to set the "details" property
        setDetails(request, authRequest);
        //具体的认证逻辑还是交给 AuthenticationManager 对象的 authenticate(..) 方法完成,接着往下看
        return this.getAuthenticationManager().authenticate(authRequest);
    }
}

AuthenticationManager 인터페이스 구현 클래스 ProviderManager

public class ProviderManager implements AuthenticationManager, MessageSourceAware,
        InitializingBean {
    ...
    private List<authenticationprovider> providers = Collections.emptyList();
    ...
    
    public Authentication authenticate(Authentication authentication)
            throws AuthenticationException {
        ....
        //遍历所有的 AuthenticationProvider, 找到合适的完成身份验证
        for (AuthenticationProvider provider : getProviders()) {
            if (!provider.supports(toTest)) {
                continue;
            }
            ...
            try {
                //进行具体的身份验证逻辑, 这里使用到的是 DaoAuthenticationProvider, 具体逻辑记着往下看
                result = provider.authenticate(authentication);

                if (result != null) {
                    copyDetails(authentication, result);
                    break;
                }
            }
            catch 
            ...
        }
        ...
        throw lastException;
    }
}</authenticationprovider>

인터페이스

에 의해 최종 분석이 완료되는 것을 소스 코드 중단점 추적을 통해 알 수 있습니다.

public abstract class AbstractUserDetailsAuthenticationProvider implements
        AuthenticationProvider, InitializingBean, MessageSourceAware {
    ...
    private UserDetailsChecker preAuthenticationChecks = new DefaultPreAuthenticationChecks();
    private UserDetailsChecker postAuthenticationChecks = new DefaultPostAuthenticationChecks();
    ...

    public Authentication authenticate(Authentication authentication)
            throws AuthenticationException {
        ...
        // 获得提交过来的用户名
        String username = (authentication.getPrincipal() == null) ? "NONE_PROVIDED"
                : authentication.getName();
        //根据用户名从缓存中查找 UserDetails
        boolean cacheWasUsed = true;
        UserDetails user = this.userCache.getUserFromCache(username);

        if (user == null) {
            cacheWasUsed = false;

            try {
                //缓存中没有则通过 retrieveUser(..) 方法查找 (看下面 DaoAuthenticationProvider 的实现)
                user = retrieveUser(username,
                        (UsernamePasswordAuthenticationToken) authentication);
            }
            catch 
            ...
        }

        try {
            //比对前的检查,例如账户以一些状态信息(是否锁定, 过期...)
            preAuthenticationChecks.check(user);
            //子类实现比对规则 (看下面 DaoAuthenticationProvider 的实现)
            additionalAuthenticationChecks(user,
                    (UsernamePasswordAuthenticationToken) authentication);
        }
        catch (AuthenticationException exception) {
            if (cacheWasUsed) {
                // There was a problem, so try again after checking
                // we're using latest data (i.e. not from the cache)
                cacheWasUsed = false;
                user = retrieveUser(username,
                        (UsernamePasswordAuthenticationToken) authentication);
                preAuthenticationChecks.check(user);
                additionalAuthenticationChecks(user,
                        (UsernamePasswordAuthenticationToken) authentication);
            }
            else {
                throw exception;
            }
        }

        postAuthenticationChecks.check(user);

        if (!cacheWasUsed) {
            this.userCache.putUserInCache(user);
        }

        Object principalToReturn = user;

        if (forcePrincipalAsString) {
            principalToReturn = user.getUsername();
        }
        //根据最终user的一些信息重新生成具体详细的 Authentication 对象并返回 
        return createSuccessAuthentication(principalToReturn, authentication, user);
    }
    //具体生成还是看子类实现
    protected Authentication createSuccessAuthentication(Object principal,
            Authentication authentication, UserDetails user) {
        // Ensure we return the original credentials the user supplied,
        // so subsequent attempts are successful even with encoded passwords.
        // Also ensure we return the original getDetails(), so that future
        // authentication events after cache expiry contain the details
        UsernamePasswordAuthenticationToken result = new UsernamePasswordAuthenticationToken(
                principal, authentication.getCredentials(),
                authoritiesMapper.mapAuthorities(user.getAuthorities()));
        result.setDetails(authentication.getDetails());

        return result;
    }
}

接下来我们来看下 DaoAuthenticationProvider 里面的三个重要的方法,比对方式、获取需要比对的 UserDetails 对象以及生产最终返回 Authentication 的方法。

public class DaoAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider {
    ...
    //密码比对
    @SuppressWarnings("deprecation")
    protected void additionalAuthenticationChecks(UserDetails userDetails,
            UsernamePasswordAuthenticationToken authentication)
            throws AuthenticationException {
        if (authentication.getCredentials() == null) {
            logger.debug("Authentication failed: no credentials provided");

            throw new BadCredentialsException(messages.getMessage(
                    "AbstractUserDetailsAuthenticationProvider.badCredentials",
                    "Bad credentials"));
        }

        String presentedPassword = authentication.getCredentials().toString();
        //通过 PasswordEncoder 进行密码比对, 注: 可自定义
        if (!passwordEncoder.matches(presentedPassword, userDetails.getPassword())) {
            logger.debug("Authentication failed: password does not match stored value");

            throw new BadCredentialsException(messages.getMessage(
                    "AbstractUserDetailsAuthenticationProvider.badCredentials",
                    "Bad credentials"));
        }
    }

    //通过 UserDetailsService 获取 UserDetails
    protected final UserDetails retrieveUser(String username,
            UsernamePasswordAuthenticationToken authentication)
            throws AuthenticationException {
        prepareTimingAttackProtection();
        try {
            //通过 UserDetailsService 获取 UserDetails
            UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
            if (loadedUser == null) {
                throw new InternalAuthenticationServiceException(
                        "UserDetailsService returned null, which is an interface contract violation");
            }
            return loadedUser;
        }
        catch (UsernameNotFoundException ex) {
            mitigateAgainstTimingAttack(authentication);
            throw ex;
        }
        catch (InternalAuthenticationServiceException ex) {
            throw ex;
        }
        catch (Exception ex) {
            throw new InternalAuthenticationServiceException(ex.getMessage(), ex);
        }
    }

    //生成身份认证通过后最终返回的 Authentication, 记录认证的身份信息
    @Override
    protected Authentication createSuccessAuthentication(Object principal,
            Authentication authentication, UserDetails user) {
        boolean upgradeEncoding = this.userDetailsPasswordService != null
                && this.passwordEncoder.upgradeEncoding(user.getPassword());
        if (upgradeEncoding) {
            String presentedPassword = authentication.getCredentials().toString();
            String newPassword = this.passwordEncoder.encode(presentedPassword);
            user = this.userDetailsPasswordService.updatePassword(user, newPassword);
        }
        return super.createSuccessAuthentication(principal, authentication, user);
    }
}

本篇文章到这里就已经全部结束了,更多其他精彩内容可以关注PHP中文网的Java教程视频栏目!

위 내용은 Spring Security 원칙 소개(코드 포함)의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!

성명:
이 기사는 segmentfault.com에서 복제됩니다. 침해가 있는 경우 admin@php.cn으로 문의하시기 바랍니다. 삭제