Spring Security 权限控制原理《一》

原创
2017/03/03 12:00
阅读数 1.2K

姗姗来迟,今天终于是写完了。。。@Jeanie 不写了,

Spring Security is a powerful and highly customizable authentication and access-control framework. It is the de-facto standard for securing Spring-based applications.(Spring安全是一个功能强大且高度可定制的身份验证和访问控制框架。这是事实上的标准,以确保Spring为基础的应用程序)看着有点糊涂。。。

Spring Security是一个能够为基于Spring的企业应用系统提供声明式的安全访问控制解决方案的安全框架。它提供了一组可以在Spring应用上下文中配置的Bean,充分利用了Spring IoC,DI(控制反转Inversion of Control ,DI:Dependency Injection 依赖注入)和AOP(面向切面编程)功能,为应用系统提供声明式的安全访问控制功能,减少了为企业系统安全控制编写大量重复代码的工作。

下面我就简单的学习了解了一下,在spring的官网上最新的已经更新到 4.2.3版本,添加maven 依赖

<dependencies>
    <dependency>
        <groupId>org.springframework.security</groupId>
        <artifactId>spring-security-web</artifactId>
        <version>4.2.3.BUILD-SNAPSHOT</version>
    </dependency>
</dependencies><repositories>
    <repository>
        <id>spring-snapshots</id>
        <name>Spring Snapshots</name>
        <url>https://repo.spring.io/libs-snapshot</url>
        <snapshots>
            <enabled>true</enabled>
        </snapshots>
    </repository>
</repositories>

** 自动导入了spring-security-web-4.2.3,和 spring-secrity-core-4.2.3这两个 jar包文件(发现运行出现不知所措的异常,所以决定版本导入为:3.1.4的jar包,具体原因可能是jarb包冲突导致的) **

web.xml配置文件

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://java.sun.com/xml/ns/javaee" xmlns:web="http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd" id="WebApp_ID" version="2.5">
  <display-name>Archetype Created Web Application</display-name>
<!-- 加载具体的配置文件。。 -->
	<context-param>
		<param-name>contextConfigLocation</param-name>
		<param-value>classpath*:classpath*:dispacher-servlet.xml,
		classpath*:spring-security.xml</param-value>
		
	</context-param>
	<context-param>
    <param-name>log4jConfigLocation</param-name>
    <param-value>/WEB-INF/classes/log4j.properties</param-value>
  </context-param>
  <!-- 添加spring监听器,必须的 -->
	<listener>
		<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
	</listener>
	<!-- 编码格式UTF-8 -->
	<filter>
		<filter-name>CharacterEncodingFilter</filter-name>
		<filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
		<init-param>
			<param-name>encoding</param-name>
			<param-value>UTF-8</param-value>
		</init-param>
		<init-param>
			<param-name>forceEncoding</param-name>
			<param-value>true</param-value>
		</init-param>
	</filter>
	<filter-mapping>
		<filter-name>CharacterEncodingFilter</filter-name>
		<url-pattern>/*</url-pattern>
	</filter-mapping>

	<!-- 添加 spring secrity 过滤器-->
    <filter>
       <filter-name>springSecurityFilterChain</filter-name>
      <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
    </filter> 
    <filter-mapping> 
       <filter-name>springSecurityFilterChain</filter-name>
       <url-pattern>/*</url-pattern>
    </filter-mapping>
	<!-- Spring view分发器 -->
	<servlet>
		<servlet-name>dispatcher</servlet-name>
		<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
		<init-param>
			<param-name>contextConfigLocation</param-name>
			<param-value>classpath:dispacher-servlet.xml</param-value>
		</init-param>
		<load-on-startup>1</load-on-startup>
	</servlet>
	<servlet-mapping>
		<servlet-name>dispatcher</servlet-name>
		<url-pattern>/</url-pattern>
	</servlet-mapping>
  <welcome-file-list>
  	<welcome-file>index.jsp</welcome-file>
  </welcome-file-list>
</web-app>

spring-security.xml 配置文件

<beans xmlns="http://www.springframework.org/schema/beans"
  xmlns:security="http://www.springframework.org/schema/security"
  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-3.1.xsd
          http://www.springframework.org/schema/security
          http://www.springframework.org/schema/security/spring-security-3.1.xsd">

	 index.jsp登录页面 和登录失败页面  配置不要 角色权限就可以访问,否则 登录失败 就不会进入到 登录失败页面。
	<security:http security="none" pattern="/index.jsp"></security:http>
 	<security:http security="none" pattern="/loginFailure.jsp"></security:http> 
<!--  	<security:http security="none" pattern="/loginSuccess.jsp"></security:http>  -->
	   <security:http auto-config="true" use-expressions="true">
	   		<!-- 登录 页面,以及登录成功页面 -->
	   		<!-- login-processing-url 这个配置 ,加上和去掉都没有什么影响。。 -->
	   		<security:form-login login-page="/index.jsp" 
	   			default-target-url="/loginSuccess.jsp"
	   			password-parameter="user_pwd"
	   			username-parameter="user_name"
	   			always-use-default-target="true"
	   			
	   			authentication-failure-url="/loginFailure.jsp"
	   			login-processing-url="/login/loginAction"/>
	   			<!-- 退出登录 以及推出成功自动跳转到 登录页面  推出失败注销 session会话-->
	   		<security:logout invalidate-session="true" 
	   				logout-success-url="/index.jsp"
	   				logout-url="/login/loginOut"/>
	   		<!-- 这个地方表示所有的用户请求,必要要有 ROLE_USER 角色 -->		
	      	<security:intercept-url pattern="/**" access="hasRole('ROLE_USER')"/>
	   </security:http>    

		<!--  authentication-manager  固定用户名和密码配置如下 没有什么实际作用-->
<!-- 	   <security:authentication-manager> -->
<!-- 	      <security:authentication-provider> -->
<!-- 	         <security:user-service> -->
<!-- 	            <security:user name="user" password="user" authorities="ROLE_USER"/> -->
<!-- 	            <security:user name="admin" password="admin" authorities="ROLE_USER, ROLE_ADMIN"/> -->
<!-- 	         </security:user-service> -->
<!-- 	      </security:authentication-provider> -->
<!-- 	   </security:authentication-manager> -->
		<!-- 自定义  authentication-manager -->
		<!-- 数据库 验证用户名  配置,我们待会主要实现这个功能 -->
		<security:authentication-manager>
                    这个配置为会set给DaoAuthenticationProvider 类中的userDetailsService,后面后说到
			<security:authentication-provider user-service-ref="userServiceImpl">
				<!-- MD5加密 密码-->
				<security:password-encoder ref="passwordEncoder"></security:password-encoder>
			</security:authentication-provider>
		</security:authentication-manager>
		<bean  id="passwordEncoder" class="org.springframework.security.authentication.encoding.Md5PasswordEncoder"/>  
</beans>

spring-context.xml 配置文件 输入图片说明

编写UserServiceImpl 这个类,实现UserDetailsService 接口,并重写loadUserByUsername这个方法,由于没有连接数据库区根据用户名获取用户信息,所以我就暂时写了一个userdb.getUser(String uname)来根据用户名去获取list集合中的一个用户信息。

@Service("userServiceImpl")
public class UserServiceImpl implements UserDetailsService{
	private Log log = LogFactory.getLog(UserServiceImpl.class);
	@Autowired
	private HttpServletRequest request;
	@Override
	public UserDetails loadUserByUsername(String username)
			throws UsernameNotFoundException {
		// TODO Auto-generated method stub
		userDB userdb = new userDB();
		UserDetails userDetails = null;
		try {
			user userInfo = userdb.getUser(username);
			userDetails = new User(userInfo.getUserName(), userInfo.getUserPassword(), getAuthority(userInfo.getAccess()));
		} catch (Exception e) {
			request.getSession().setAttribute("loginInfo",e.getMessage());
			log.error(e.getMessage());
			
			e.printStackTrace();
		}
		return userDetails;
	}
	
	权限给予
	private List<GrantedAuthority> getAuthority(Integer access){
		List<GrantedAuthority> authorityList = new ArrayList<GrantedAuthority>();
		if(access.intValue() == 0){
			authorityList.add(new GrantedAuthorityImpl("ROLE_USER"));
			authorityList.add(new GrantedAuthorityImpl("ROLE_ADMIN"));
		}else{
			authorityList.add(new GrantedAuthorityImpl("ROLE_USER"));
		}
		return authorityList;
	}

}

userDB类

public class userDB {
	private static  List<user> userDB = new ArrayList<user>();
	static{
		user u1 = new user("admin","21232f297a57a5a743894a0e4a801fc3",0);
		user u2 = new user("user","ee11cbb19052e40b07aac0ca060c23ee",1);
		userDB.add(u1);
		userDB.add(u2);
	}
	/*** @throws Exception **/
	public user getUser(String name) throws UsernameNotFoundException{
		if(name == null || "".equals(name)){
			throw new UsernameNotFoundException("username could not null");
		}
		for (user u : userDB) {
			System.out.println(u.getUserName().equals(name));
			if(u.getUserName().equals(name)){
				return u;
			}
		}
		throw new UsernameNotFoundException("user dose not exist");
	}
}

在web.xml中添加了对请求拦截的过滤器类DelegatingFilterProxy.java.通过断点doFilter方法查看可以知道一个请求执行了一下过滤器链类 其中有一个UsernamePasswordAuthenticationFilter 这个是我们验证用户名和密码的类

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

看到有一个 this.getAuthenticationManager() 这个方法来自父类AbstractAuthenticationProcessingFilter方法 获取到的是AuthenticationManager. 在进入authenticate(authRequest)方法查看

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

在这其中 getProviders 获取到的是 一个 List<AuthenticationProvider> 集合,循环的去调用一个authenticate()方法,在集合中有一个 AbstractUserDetailsAuthenticationProvider 抽象类,当对这个类的方发被执行时

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

有一个retrieveUser()和 additionalAuthenticationChecks()被执行,而这个抽象类被DaoAuthenticationProvider所继承实现,所以调用这个retrieveUser执行的是其子类所实现的具体方法。在其实现类中我们可以看到是如何对用户名和密码进行验证的

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

    protected void doAfterPropertiesSet() throws Exception {
        Assert.notNull(this.userDetailsService, "A UserDetailsService must be set");
    }

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

其中additionalAuthenticationChecks 方法是对密码的验证。retrieveUser是根据用户输入的用户名去获取一个具体的用户信息实体类,其中就包含我们的用户名和密码。在根据用户名去获取用户信息实体类时:this.getUserDetailsService() 这个方法其实就是获取到我们自己之前定义的UserServiceImpl 实现了UserDetailsService接口的类,并把这个类在spring-secret.xml中set到我们的DaoAuthenticationProvider类中的userDetailsService,这样就完成了对用户权限的验证。。。

界面效果: 输入图片说明 输入图片说明 输入图片说明

展开阅读全文
打赏
0
7 收藏
分享
加载中
更多评论
打赏
0 评论
7 收藏
0
分享
返回顶部
顶部