首先提供一个整体的配置文件,再分析:
<?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,即一个权限判断的提供者,它又包含了三个属性,主要关注的是userDetailsService和passwordEncoder
即用户名和密码的校验,后面跟进代码会再次回到这两个属性上~!(其实用户名和密码都正确的话,理论上就是通过了登陆权限的校验)
看看这个过滤器,它主要的功能是由方法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,该方法需要我们自己实现,主要就是通过用户名从数据库中获取用户并判断
跟进这么多,最终得到如同上面说的结果:回到userDetailsService和passwordEncoder
首先是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);
可以看到这个方法最终会执行我们自己继承的类
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);
}
这里可以看到倒数第二个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
其中的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使用
后面就返回了,继续执行过滤器链