Spring Security的自定义登录如何工作

and*_*ndy 3 java authentication login spring-security logout

我正试图通过Spring Security.我必须实现自定义登录表单,所以我需要很好地理解我的配置意味着什么.

弹簧security.xml文件

<beans:beans xmlns="http://www.springframework.org/schema/security"
    xmlns:beans="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://www.springframework.org/schema/beans
    http://www.springframework.org/schema/beans/spring-beans-4.0.xsd
    http://www.springframework.org/schema/security
    http://www.springframework.org/schema/security/spring-security-4.0.xsd">

    <http auto-config="true">
        <intercept-url pattern="/user**" access="isAuthenticated()" />
        <form-login authentication-failure-url="/login" login-page="/login"
            login-processing-url="/login" default-target-url="/user" />
        <logout invalidate-session="true" logout-success-url="/index"
            logout-url="/logout" />
    </http>

    <authentication-manager id="custom-auth">
        <authentication-provider>
            <user-service>
                <user name="my_username" password="my_password"
                    authorities="ROLE_USER" />
            </user-service>
        </authentication-provider>
    </authentication-manager>
Run Code Online (Sandbox Code Playgroud)

的LoginController

@Controller
public class LoginController {
    [....]

    @RequestMapping(value = "/login", method = RequestMethod.POST)
    public ModelAndView doLogin() {
        System.out.println("***LOGIN_POST***");
        return new ModelAndView("users/home");
    }

    @RequestMapping(value = "/logout", method = RequestMethod.POST)
    public ModelAndView doLogout() {
        System.out.println("***LOGOUT_POST***");
        return new ModelAndView("index");
    }
}
Run Code Online (Sandbox Code Playgroud)

我知道我可以使用RequestMethod.GET映射/ login URL,但是当我尝试在表单提交后拦截POST时它不起作用.

  1. 我相信,但需要确认,这是因为安全性在幕后做了一些事情:从发布的表单中获取用户名和密码值,并将它们与身份验证提供程序中的值进行比较:如果匹配,则显示default-target-url ,否则用户必须重复登录.这样对吗?
  2. 然后我的问题是:我需要在安全性的登录表单中输入用户名和密码值,因为我必须向外部服务器发送HTTP请求以验证这些是否匹配.在介绍安全性之前,我使用/ login GET和/ login POST,使用@ModelAttribute注释开发了这个机制.我现在该怎么办?
  3. 使用实现UserDetailsS​​ervice的类更改身份验证提供程序,会发生什么?我相信,在这种情况下,登录表单中输入的用户名和密码将与从db检索到的用户名和密码进行比较,因为这些用户名和密码已分配给User对象.这样对吗?

UserDetailsS​​erviceImpl

    @Service
    public class UserDetailsServiceImpl implements UserDetailsService {
        @Autowired
        private CustomerDao customerDao;

        @Override
        public UserDetails loadUserByUsername(String username)
                throws UsernameNotFoundException {
            Customer customer = customerDao.findCustomerByUsername(username);
            return new User(customer.getUsername(), customer.getPassword(), true, true, true, true,
                Arrays.asList(new SimpleGrantedAuthority(customer.getRole())));
        }
    }
Run Code Online (Sandbox Code Playgroud)

NB 用户的数据最初不在我的数据库中,这是因为我不确定UserDetailsS​​ervice解决方案(其中UserDetails只是通过用户名加载).要检索我的Customer对象,我需要用户名和密码(发送到特定的外部URL)然后,如果JSON响应是肯定的(用户名和密码是正确的),我必须发送2个其他HTTP请求以获取客户的数据作为名字,姓氏,国籍等.此时我的用户可以被视为已登录.

有什么建议?提前致谢.

jlu*_*etu 6

  1. 我相信,但需要确认,这是因为安全性在幕后做了一些事情:从发布的表单中获取用户名和密码值,并将它们与身份验证提供程序中的值进行比较:如果匹配,则显示default-target-url ,否则用户必须重复登录.这样对吗?

那就对了.<login-form>在安全配置中声明元素时,您正在配置UsernamePasswordAuthenticationFilter.

你在那里配置一些网址:

  • login-page ="/ login":指向@RequestMapping返回登录表单的url的url
  • login-processing-url ="/ login":触发UsernamePasswordAuthenticationFilter的url.这spring-security相当于构建后处理控制器方法.
  • default-target-url ="/ user":提供有效用户凭据后将重定向用户的默认页面.
  • authentication-failure-url ="/ login":尝试使用无效凭据登录时将重定向用户的URL.

虽然login-processing-url,default-target-url和authentication-failure-url必须是有效的RequestMappings,但login-processing-url将不会到达Spring MVC控制器层,因为它在执行Spring MVC调度程序servlet之前执行.

所以

@RequestMapping(value = "/login", method = RequestMethod.POST)
    public ModelAndView doLogin() {
        System.out.println("***LOGIN_POST***");
        return new ModelAndView("users/home");
    }
Run Code Online (Sandbox Code Playgroud)

不会永远达不到.

将帖子归档到/loginuri时,UsernamePasswordAuthenticationFilter将执行它doFilter()捕获用户提供的凭据的方法,构建UsernamePasswordAuthenticationToken并将其委托给AuthenticationManager,其中此身份验证将在匹配的AuthenticationProvider中执行.

  1. 然后我的问题是:我需要在安全性的登录表单中输入用户名和密码值,因为我必须向外部服务器发送HTTP请求以验证这些是否匹配.在介绍安全性之前,我使用/ login GET和/ login POST,使用@ModelAttribute注释开发了这个机制.我现在该怎么办?我想你曾经在外部服务器上执行身份验证时通过从POST /登录RequestMapping委托一个类来完成它.

因此,只需创建一个自定义AuthenticationProvider,它将用户验证内容委托给您的旧逻辑:

public class ThirdPartyAuthenticationProvider implements AuthenticationProvider {

    private Class<? extends Authentication> supportingClass = UsernamePasswordAuthenticationToken.class;

    // This represents your existing username/password validation class
    // Bind it with an @Autowired or set it in your security config
    private ExternalAuthenticationValidator externalAuthenticationValidator;

    /* (non-Javadoc)
     * @see org.springframework.security.authentication.AuthenticationProvider#authenticate(org.springframework.security.core.Authentication)
     */
    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        boolean validated = this.externalAuthenticationValidator.validate(authentication.getName(), authentication.getCredentials().toString());
        if(!validated){
            throw new BadCredentialsException("username and/or password not valid");
        }
        Collection<? extends GrantedAuthority> authorities = null; 
        // you must fill this authorities collection
        return new UsernamePasswordAuthenticationToken(
                    authentication.getName(),
                    authentication.getCredentials(),
                    authorities
                );      
    }

    /* (non-Javadoc)
     * @see org.springframework.security.authentication.AuthenticationProvider#supports(java.lang.Class)
     */
    @Override
    public boolean supports(Class<?> authentication) {
        return this.supportingClass.isAssignableFrom(authentication);
    }

    public ExternalAuthenticationValidator getExternalAuthenticationValidator() {
        return externalAuthenticationValidator;
    }

    public void setExternalAuthenticationValidator(ExternalAuthenticationValidator externalAuthenticationValidator) {
        this.externalAuthenticationValidator = externalAuthenticationValidator;
    }   

}
Run Code Online (Sandbox Code Playgroud)

和安全配置xml:

    <beans:bean id="thirdPartyAuthenticationProvider" class="com.xxx.yyy.ThirdPartyAuthenticationProvider">
        <!-- here set your external authentication validator in case you can't autowire it -->
        <beans:property name="externalAuthenticationValidator" ref="yourExternalAuthenticationValidator" />
    </beans:bean>

    <security:authentication-manager id="custom-auth">
        <security:authentication-provider ref="thirdPartyAuthenticationProvider" />
    </security:authentication-manager>

    <security:http auto-config="true" authentication-manager-ref="custom-auth">
        <security:intercept-url pattern="/user**" access="isAuthenticated()" />
        <security:form-login authentication-failure-url="/login" login-page="/login"
            login-processing-url="/login" default-target-url="/user" />
        <security:logout invalidate-session="true" logout-success-url="/index"
            logout-url="/logout" />
        <!-- in spring security 4.x CSRF filter is enabled by default. Disable it if 
             you don't plan to use it, or at least in the first attempts -->
        <security:csrf disabled="true"/>
    </security:http>
Run Code Online (Sandbox Code Playgroud)
  1. 使用实现UserDetailsS​​ervice的类更改身份验证提供程序,会发生什么?我相信,在这种情况下,登录表单中输入的用户名和密码将与从db检索到的用户名和密码进行比较,因为这些用户名和密码已分配给User对象.这样对吗?

正如您所说,您必须同时发送用户名和密码,我认为UserServiceDetails架构并不符合您的要求.我瘦了你应该按照我在第2点的建议去做.

编辑:

最后一件事:现在我在身份验证方法中发送HTTP请求,如果凭据正确,我会在响应中收到一个令牌,我需要访问其他外部服务器服务.如何在Spring控制器中传递它?

要接收和处理收到的令牌,我会这样做:

ExternalAuthenticationValidator接口:

public interface ExternalAuthenticationValidator {

    public abstract ThirdPartyValidationResponse validate(String name, String password);

}
Run Code Online (Sandbox Code Playgroud)

ThirdPartyValidationResponse模型接口:

public interface ThirdPartyValidationResponse{

    public boolean isValid();

    public Serializable getToken();

}
Run Code Online (Sandbox Code Playgroud)

然后,更改提供者处理和管理它的方式:

public class ThirdPartyAuthenticationProvider implements AuthenticationProvider {

    private Class<? extends Authentication> supportingClass = UsernamePasswordAuthenticationToken.class;

    private ExternalAuthenticationValidator externalAuthenticationValidator;

    /* (non-Javadoc)
     * @see org.springframework.security.authentication.AuthenticationProvider#authenticate(org.springframework.security.core.Authentication)
     */
    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        ThirdPartyValidationResponse response = this.externalAuthenticationValidator.validate(authentication.getName(), authentication.getCredentials().toString());
        if(!response.isValid()){
            throw new BadCredentialsException("username and/or password not valid");
        }
        Collection<? extends GrantedAuthority> authorities = null; 
        // you must fill this authorities collection
        UsernamePasswordAuthenticationToken authenticated =  
                new UsernamePasswordAuthenticationToken(
                    authentication.getName(),
                    authentication.getCredentials(),
                    authorities
                );
        authenticated.setDetails(response);
        return authenticated;
    }

    /* (non-Javadoc)
     * @see org.springframework.security.authentication.AuthenticationProvider#supports(java.lang.Class)
     */
    @Override
    public boolean supports(Class<?> authentication) {
        return this.supportingClass.isAssignableFrom(authentication);
    }

    public ExternalAuthenticationValidator getExternalAuthenticationValidator() {
        return externalAuthenticationValidator;
    }

    public void setExternalAuthenticationValidator(ExternalAuthenticationValidator externalAuthenticationValidator) {
        this.externalAuthenticationValidator = externalAuthenticationValidator;
    }   

}
Run Code Online (Sandbox Code Playgroud)

现在,您必须使用此代码段从userDetails中检索令牌:

        SecurityContext context = SecurityContextHolder.getContext();
        Authentication auth = context.getAuthentication();
        if(auth == null){
            throw new IllegalAccessException("Authentication is null in SecurityContext");
        }
        if(auth instanceof UsernamePasswordAuthenticationToken){
            Object details = auth.getDetails();
            if(details != null && details instanceof ThirdPartyValidationResponse){
                return ((ThirdPartyValidationResponse)details).getToken();
            }
        }
        return null;
Run Code Online (Sandbox Code Playgroud)

不是将它包含在您需要的任何地方,而是创建一个从验证的详细信息中检索它的类可能更好:

public class SecurityContextThirdPartyTokenRetriever {

    public Serializable getThirdPartyToken() throws IllegalAccessException{
        SecurityContext context = SecurityContextHolder.getContext();
        Authentication auth = context.getAuthentication();
        if(auth == null){
            throw new IllegalAccessException("Authentication is null in SecurityContext");
        }
        if(auth instanceof UsernamePasswordAuthenticationToken){
            Object details = auth.getDetails();
            if(details != null && details instanceof ThirdPartyValidationResponse){
                return ((ThirdPartyValidationResponse)details).getToken();
            }
        }
        return null;
    }

}
Run Code Online (Sandbox Code Playgroud)

如果您选择了这种最后一种方式,只需在security xml config中声明它(或使用@Service等注释注释):

<beans:bean id="tokenRetriever" class="com.xxx.yyy.SecurityContextThirdPartyTokenRetriever" />
Run Code Online (Sandbox Code Playgroud)

还有其他方法,例如扩展UsernamePasswordAuthenticationToken以将令牌作为字段包含在其中,但这是我认为最简单的方法.