SpringSecurity总结

首先提供一个整体的配置文件,再分析:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:s="http://www.springframework.org/schema/security"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:context="http://www.springframework.org/schema/context"
    xsi:schemaLocation="
        http://www.springframework.org/schema/beans 
        http://www.springframework.org/schema/beans/spring-beans-3.1.xsd
        http://www.springframework.org/schema/security
        http://www.springframework.org/schema/security/spring-security-3.1.xsd
        http://www.springframework.org/schema/context 
        http://www.springframework.org/schema/context/spring-context-3.1.xsd">
        
    <!-- 不要过滤图片等静态资源,其中**代表可以跨越目录,*不可以跨越目录。 -->
	<s:http pattern="/**/*.jpg" security="none"/>
	<s:http pattern="/**/*.png" security="none"/>
	<s:http pattern="/**/*.gif" security="none"/>
	<s:http pattern="/**/*.css" security="none"/>
	<s:http pattern="/**/*.js" security="none"/>
	<s:http pattern="/login.jsp" security="none"/>
	<s:http pattern="/user/user!login.action" security="none"/>
	
	<s:http auto-config="false" use-expressions="true" entry-point-ref="loginUrlEntryPoint" access-denied-page="/error.jsp">
		<!-- 处理用户登陆时,用户名和密码判断的filter(重要) -->
		<s:custom-filter position="FORM_LOGIN_FILTER" ref="loginFilter"/>
		<!-- 权限判断、处理的filter链 -->
		<s:custom-filter before="FILTER_SECURITY_INTERCEPTOR" ref="myFilter"/>
		<!-- 检测失效的sessionId,超时时定位到另外一个URL -->
		<s:session-management invalid-session-url="/sessionTimeout.jsp" />
		<!-- 登出 -->
		<s:logout logout-url="/j_spring_security_logout" invalidate-session="true" logout-success-url="/index.jsp" />
	</s:http>
    
	<!-- 一个自定义的filter,必须包含authenticationManager, accessDecisionManager,securityMetadataSource三个属性。 -->
	<bean id="myFilter" class="com.dtds.security.MyFilterSecurityInterceptor">
		<!-- 资源数据源 -->
		<property name="securityMetadataSource" ref="mySecurityMetadataSource" />
		<!-- 认证管理器 -->
		<property name="authenticationManager" ref="authenticationManager" />
		<!-- 访问决策器 -->
		<property name="accessDecisionManager" ref="accessDecisionManager" />
	</bean>
		
	<!-- 资源源数据定义,将所有的资源和权限对应关系建立起来,即定义某一资源可以被哪些角色去访问。 -->
	<bean id="mySecurityMetadataSource" class="com.dtds.security.InvocationSecurityMetadataSourceServiceImpl">
		<property name="userService" ref="userService"/>
	</bean>

	<!-- 认证配置, 使用userDetailsService提供的用户信息 -->
	<s:authentication-manager alias="authenticationManager">
		<s:authentication-provider ref="authenticationProvider"/>
	</s:authentication-manager>
	
	<!-- 可以重写 -->
	<!-- 
	<bean id="authenticationProvider" class="org.springframework.security.authentication.dao.DaoAuthenticationProvider">  
	 -->
	<bean id="authenticationProvider" class="com.dtds.security.SecurityAuthenticationProvider">  
	    <property name="userDetailsService" ref="userDetailsService" />
	    <property name="hideUserNotFoundExceptions" value="false" />
	    <property name="passwordEncoder" ref="passwordEncoder"/>
	</bean>
    <!-- 登陆时查询用户、并加载用户所拥有的权限等 -->
	<bean id="userDetailsService" class="com.dtds.security.UserDetailServiceImpl">
		<property name="userService" ref="userService"/>
	</bean>
	<!-- 用户的密码加密或解密 -->
	<bean id="passwordEncoder" class="com.dtds.security.MyPasswordEncoder" />
	
	<!-- 访问决策器,决定某个用户具有的角色,是否有足够的权限去访问某个资源。 -->
	<bean id="accessDecisionManager" class="com.dtds.security.MyAccessDecisionManager"/>

	<!-- 重写登陆验证 -->
	<bean id="loginFilter" class="com.dtds.security.MyUsernamePasswordAuthenticationFilter">
		<!-- 认证管理 -->
		<property name="authenticationManager" ref="authenticationManager"></property>
		<!-- 验证成功后的跳转 -->
		<property name="authenticationSuccessHandler" ref="loginLogAuthenticationSuccessHandler"></property>
		<!-- 处理登陆的Action -->
		<property name="filterProcessesUrl" value="/j_spring_security_check"></property>
		<!-- 验证失败后的处理 -->
		<property name="authenticationFailureHandler" ref="simpleUrlAuthenticationFailureHandler"></property>
	</bean>
	<!-- 登陆成功 -->
	<!-- 这里要实现自定义 -->
	<!-- 
	<bean id="loginLogAuthenticationSuccessHandler" class="org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler">
	 -->
	<bean id="loginLogAuthenticationSuccessHandler" class="com.dtds.security.LoginAuthenticationSuccessHandler">
		<property name="alwaysUseDefaultTargetUrl" value="true"/>
		<!-- 登陆成功时的页面,这里设定的页面也会被spring security拦截 -->
		<!-- 
		<property name="defaultTargetUrl" value="/content/select.jsp" />
		<property name="targetUrlParameter" value="redirectTo" />
		 -->
		<property name="defaultTargetUrl" value="/user/user!login.action" />
	</bean>
	<!-- 登陆失败 -->
	<bean id="simpleUrlAuthenticationFailureHandler" class="org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler">
		<!-- 可以配置相应的跳转方式。属性forwardToDestination为true采用forward false为sendRedirect -->
		<property name="defaultFailureUrl" value="/login.jsp"></property>
	</bean>
	<!-- 未登录的切入点 -->
	<bean id="authenticationProcessingFilterEntryPoint" class="org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint">
		<property name="loginFormUrl" value="/login.jsp"></property>
	</bean>
	
	<!-- Spring Security 认证切入点 -->
	<bean id="loginUrlEntryPoint" class="org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint">
		<property name="loginFormUrl" value="/login.jsp"></property>
	</bean>
	
</beans>

 

首先看到,用户登陆的时候吗请求会被这个配置拦截:

<!-- 处理用户登陆时,用户名和密码判断的filter(重要) -->
		<s:custom-filter position="FORM_LOGIN_FILTER" ref="loginFilter"/>

可以看到这个loginFilter是

<!-- 重写登陆验证 -->
	<bean id="loginFilter" class="com.dtds.security.MyUsernamePasswordAuthenticationFilter">

它包含了四个属性,首先看第一个

authenticationManager

它最终指向的是authenticationProvider,即一个权限判断的提供者,它又包含了三个属性,主要关注的是userDetailsServicepasswordEncoder

即用户名和密码的校验,后面跟进代码会再次回到这两个属性上~!(其实用户名和密码都正确的话,理论上就是通过了登陆权限的校验)

 

看看这个过滤器,它主要的功能是由方法attemptAuthentication提供,最终的目的返回一个权限对象Authentication,在该方法中:

主要是这句:

Authentication authentication = super.attemptAuthentication(request, response);

由于

MyUsernamePasswordAuthenticationFilter

是继承自UsernamePasswordAuthenticationFilter过滤器,那么这里使用了super,即实际执行的是父类的方法,源码如下:

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();

        UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);

        // Allow subclasses to set the "details" property
        setDetails(request, authRequest);

        return this.getAuthenticationManager().authenticate(authRequest);
    }

该方法的入参就是request和response,接着执行,就获取到页面输入的用户名和密码,由于这里返回的是一个权限对象,可以看到最终返回的即是这句:

this.getAuthenticationManager().authenticate(authRequest);

在返回前,并未看到SS对用户名和密码做任何验证,那么验证肯定在上述这句代码中,跟进去,进入到ProviderManager的authenticate方法,源码如下:

public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        Class<? extends Authentication> toTest = authentication.getClass();
        AuthenticationException lastException = null;
        Authentication result = null;
        boolean debug = logger.isDebugEnabled();

        for (AuthenticationProvider provider : getProviders()) {
            if (!provider.supports(toTest)) {
                continue;
            }

            if (debug) {
                logger.debug("Authentication attempt using " + provider.getClass().getName());
            }

            try {
                result = provider.authenticate(authentication);

                if (result != null) {
                    copyDetails(authentication, result);
                    break;
                }
            } catch (AccountStatusException e) {
                prepareException(e, authentication);
                // SEC-546: Avoid polling additional providers if auth failure is due to invalid account status
                throw e;
            } catch (AuthenticationException e) {
                lastException = e;
            }
        }

        if (result == null && parent != null) {
            // Allow the parent to try.
            try {
                result = parent.authenticate(authentication);
            } catch (ProviderNotFoundException e) {
                // ignore as we will throw below if no other exception occurred prior to calling parent and the parent
                // may throw ProviderNotFound even though a provider in the child already handled the request
            } catch (AuthenticationException e) {
                lastException = e;
            }
        }

        if (result != null) {
            if (eraseCredentialsAfterAuthentication && (result instanceof CredentialsContainer)) {
                // Authentication is complete. Remove credentials and other secret data from authentication
                ((CredentialsContainer)result).eraseCredentials();
            }

            eventPublisher.publishAuthenticationSuccess(result);
            return result;
        }

        // Parent was null, or didn't authenticate (or throw an exception).

        if (lastException == null) {
            lastException = new ProviderNotFoundException(messages.getMessage("ProviderManager.providerNotFound",
                        new Object[] {toTest.getName()}, "No AuthenticationProvider found for {0}"));
        }

        prepareException(lastException, authentication);

        throw lastException;
    }

 

执行到这句:

result = provider.authenticate(authentication);

继续跟进,进入:AbstractUserDetailsAuthenticationProvider的authenticate方法,源码:

public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication,
            messages.getMessage("AbstractUserDetailsAuthenticationProvider.onlySupports",
                "Only UsernamePasswordAuthenticationToken is supported"));

        // Determine username
        String username = (authentication.getPrincipal() == null) ? "NONE_PROVIDED" : authentication.getName();

        boolean cacheWasUsed = true;
        UserDetails user = this.userCache.getUserFromCache(username);

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

            try {
                user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication);
            } catch (UsernameNotFoundException notFound) {
                logger.debug("User '" + username + "' not found");

                if (hideUserNotFoundExceptions) {
                    throw new BadCredentialsException(messages.getMessage(
                            "AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
                } else {
                    throw notFound;
                }
            }

            Assert.notNull(user, "retrieveUser returned null - a violation of the interface contract");
        }

        try {
            preAuthenticationChecks.check(user);
            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();
        }

        return createSuccessAuthentication(principalToReturn, authentication, user);
    }

 

这里看出ss首先去缓存中获取user对象,如果没有获取到,则继续执行,到这句

                user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication);

跟进,则进入:

DaoAuthenticationProvider的retrieveUser方法,源码:

 protected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication)
            throws AuthenticationException {
        UserDetails loadedUser;

        try {
            loadedUser = this.getUserDetailsService().loadUserByUsername(username);
        } catch (UsernameNotFoundException notFound) {
            if(authentication.getCredentials() != null) {
                String presentedPassword = authentication.getCredentials().toString();
                passwordEncoder.isPasswordValid(userNotFoundEncodedPassword, presentedPassword, null);
            }
            throw notFound;
        } catch (Exception repositoryProblem) {
            throw new AuthenticationServiceException(repositoryProblem.getMessage(), repositoryProblem);
        }

        if (loadedUser == null) {
            throw new AuthenticationServiceException(
                    "UserDetailsService returned null, which is an interface contract violation");
        }
        return loadedUser;
    }

这里关键代码:

loadedUser = this.getUserDetailsService().loadUserByUsername(username);

即使用UserDetailService的loadUserByUsername方法,而UserDetailService是一个接口,该接口正是需要我们自己实现的关键点,它只有唯一的一个方法loadUserByUsername,该方法需要我们自己实现,主要就是通过用户名从数据库中获取用户并判断

跟进这么多,最终得到如同上面说的结果:回到userDetailsServicepasswordEncoder

首先是userDetailsService

它主要是获取用户及权限,该方法返回的是一个Spring定义的User,按要求封装即可,可以自己创建一个MyUser类,继承该User或者直接返回即可

返回之后,继续在DaoAuthenticationProvider中执行,执行完毕之后继续在AbstractUserDetailsAuthenticationProvider中执行

执行到这句:

preAuthenticationChecks.check(user);

源码:

private class DefaultPreAuthenticationChecks implements UserDetailsChecker {
        public void check(UserDetails user) {
            if (!user.isAccountNonLocked()) {
                logger.debug("User account is locked");

                throw new LockedException(messages.getMessage("AbstractUserDetailsAuthenticationProvider.locked",
                        "User account is locked"), user);
            }

            if (!user.isEnabled()) {
                logger.debug("User account is disabled");

                throw new DisabledException(messages.getMessage("AbstractUserDetailsAuthenticationProvider.disabled",
                        "User is disabled"), user);
            }

            if (!user.isAccountNonExpired()) {
                logger.debug("User account is expired");

                throw new AccountExpiredException(messages.getMessage("AbstractUserDetailsAuthenticationProvider.expired",
                        "User account has expired"), user);
            }
        }
}

这是一个内部类,可以看到是判断user的一些其他属性

接着执行下面一句:

additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication);

153228_WO6F_1474131.png

可以看到这个方法最终会执行我们自己继承的类

SecurityAuthenticationProvider

方法:


@Override
	protected void additionalAuthenticationChecks(UserDetails userDetails,UsernamePasswordAuthenticationToken authentication)
			throws AuthenticationException {
		logger.info("【3】:SecurityAuthenticationProvider");
		
		//执行到这句话,下面就会执行MyPasswordEncoder
		super.additionalAuthenticationChecks(userDetails, authentication);

		WebAuthenticationDetails webDetail = (WebAuthenticationDetails) authentication.getDetails();

		try {
			this.logger.warn("开始写currentLogin表");
			long start = System.currentTimeMillis();
			//this.POService.additionalLoginCheck(peopleInfo);
			long end = System.currentTimeMillis();
			this.logger.warn("写currentLogin表完毕,用时:" + (end - start) + "ms");
		} catch (Exception e) {
			this.logger.error("写currentLogin表异常", e);
			throw new AuthenticationServiceException(e.getMessage());
		}
	}

 

这里可以添加一些其他的应用

该方法使用super又调用了父类的方法

DaoAuthenticationProvider

方法

@SuppressWarnings("deprecation")
    protected void additionalAuthenticationChecks(UserDetails userDetails,
            UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
        Object salt = null;

        if (this.saltSource != null) {
            salt = this.saltSource.getSalt(userDetails);
        }

        if (authentication.getCredentials() == null) {
            logger.debug("Authentication failed: no credentials provided");

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

        String presentedPassword = authentication.getCredentials().toString();

        if (!passwordEncoder.isPasswordValid(userDetails.getPassword(), presentedPassword, salt)) {
            logger.debug("Authentication failed: password does not match stored value");

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

 

可以看到继续是一些验证代码

其中这句:

String presentedPassword = authentication.getCredentials().toString();

获取的即是用户输入的密码,下面调用密码判断的代码:

if (!passwordEncoder.isPasswordValid(userDetails.getPassword(), presentedPassword, salt)) {
            logger.debug("Authentication failed: password does not match stored value");

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

153329_Tmwj_1474131.png

这里可以看到倒数第二个MyPassWordEncoder,即是我们自己实现的密码验证类,如下:

@Override
	public boolean isPasswordValid(String npwd, String opwd, Object arg2)
	{
		System.out.println("3:MyPasswordEncoder.isPasswordValid().......validating password..........");
		System.out.println("输入的密码:" + opwd + ",存储的密码" + npwd);
		if(npwd.equals(opwd)){
			System.out.println("密码正确");
		} else {
			System.out.println("密码错误");
		}
		return npwd.equals(opwd);
	}

这里只是简单的验证而已,可以添加其他的

自此,DaoAuthenticationProvider已经执行完毕,代码继续回到:SecurityAuthenticationProvider中,执行完毕,继续执行AbstractUserDetailsAuthenticationProvider,知道执行最后一句返回代码:

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;
}

从名字可以看出,现在已经是符合权限的要求了create-success-Authentication

看这句

UsernamePasswordAuthenticationToken result = new UsernamePasswordAuthenticationToken(principal,

                authentication.getCredentials(), authoritiesMapper.mapAuthorities(user.getAuthorities()));

这里的user就是我们自定义的继承自spring User的MyUser,由于重写了

public Collection<GrantedAuthority> getAuthorities() {
		List list = new ArrayList();
		for (String sid : this.roleInfos) {
			list.add(new GrantedAuthorityImpl(sid));
		}
		return list;
	}

所以此时执行的是我们自己的getAuthrities(),注意这里返回的result

153458_Bmuq_1474131.png

其中的principal存储的是User,所以其他方法调用getPrincipal()得到的就是User

自此AbstractUserDetailsAuthenticationProvider执行完毕

继续执行ProviderManager#authenticate()

由于result不为null,则执行:

if (result != null) {

                    copyDetails(authentication, result);

                    break;

                }

 

继续执行,则ProviderManager执行完毕,接着执行UsernamePasswordAuthenticationFilter,返回authentication,执行完毕。

再执行MyUsernamePasswordAuthenticationFilter

得到了authentication对象并返回,MyUsernamePasswordAuthenticationFilter执行完毕。

自此这个过滤器AbstractAuthenticationProcessingFilter执行到doFilter,得到正确的权限对象,最后执行:successfulAuthentication(request, response, chain, authResult);

 protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain,
            Authentication authResult) throws IOException, ServletException{
        successfulAuthentication(request, response, authResult);
}

继续执行

@Deprecated
    protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response,
            Authentication authResult) throws IOException, ServletException {

        if (logger.isDebugEnabled()) {
            logger.debug("Authentication success. Updating SecurityContextHolder to contain: " + authResult);
        }

        SecurityContextHolder.getContext().setAuthentication(authResult);

        rememberMeServices.loginSuccess(request, response, authResult);

        // Fire event
        if (this.eventPublisher != null) {
            eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(authResult, this.getClass()));
        }

        successHandler.onAuthenticationSuccess(request, response, authResult);
}

这里可以看出,将权限信息set到了SecurityContextHolder中,接着执行rememberMeService。。

最后一句:

successHandler.onAuthenticationSuccess(request, response, authResult);

由于上述的权限认证已经通过,所以这里就调用successHandler,即登录成功后的处理类,这个类已经被我们实现了,所以就执行LoginAuthenticationSuccessHandler的onAuthenticationSuccess方法,在这里面就是执行我们自己的流程,其中最重要的就是将用户对象set到session中,供整个web使用

后面就返回了,继续执行过滤器链

 

 

 

转载于:https://my.oschina.net/u/1474131/blog/1068857

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值