Spring安全示例UserDetailsService

时间:2020-02-23 14:36:01  来源:igfitidea点击:

欢迎来到使用UserDetailsService的Spring Security示例。
之前我们学习了如何在Web应用程序中使用Spring Security。
今天,我们将研究如何将Spring Security集成到Spring MVC项目中以进行身份验证。

Spring安全示例

将Spring Security与Spring MVC Framework集成非常容易,因为我们已经有了Spring Beans配置文件。
我们需要做的是创建与Spring Security身份验证相关的更改以使其正常运行。
今天,我们将研究如何使用内存中的UserDetailsService DAO实现和基于JDBC的身份验证在Spring MVC应用程序中实现身份验证。

首先在Spring Tool Suite中创建一个简单的Spring MVC项目,这将为我们提供基本的Spring MVC应用程序,以构建我们的Spring安全示例应用程序。
完成所有更改后,我们的应用程序将如下图所示。

让我们看一下我们的Spring安全示例项目的每个组件。

Spring Security Maven依赖关系

我们最终的pom.xml文件如下所示。

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="https://maven.apache.org/POM/4.0.0" xmlns:xsi="https://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="https://maven.apache.org/POM/4.0.0 https://maven.apache.org/maven-v4_0_0.xsd">
	<modelVersion>4.0.0</modelVersion>
	<groupId>com.theitroad.spring</groupId>
	<artifactId>SpringMVCSecurity</artifactId>
	<name>SpringMVCSecurity</name>
	<packaging>war</packaging>
	<version>1.0.0-BUILD-SNAPSHOT</version>
	<properties>
		<java-version>1.6</java-version>
		<org.springframework-version>4.0.2.RELEASE</org.springframework-version>
		<org.aspectj-version>1.7.4</org.aspectj-version>
		<org.slf4j-version>1.7.5</org.slf4j-version>
	</properties>
	<dependencies>
		<!-- Spring -->
		<dependency>
			<groupId>org.springframework</groupId>
			<artifactId>spring-context</artifactId>
			<version>${org.springframework-version}</version>
			<exclusions>
				<!-- Exclude Commons Logging in favor of SLF4j -->
				<exclusion>
					<groupId>commons-logging</groupId>
					<artifactId>commons-logging</artifactId>
				 </exclusion>
			</exclusions>
		</dependency>
		<dependency>
			<groupId>org.springframework</groupId>
			<artifactId>spring-webmvc</artifactId>
			<version>${org.springframework-version}</version>
		</dependency>
		<!-- Spring Security -->
		<dependency>
			<groupId>org.springframework.security</groupId>
			<artifactId>spring-security-config</artifactId>
			<version>3.2.3.RELEASE</version>
		</dependency>
		<dependency>
			<groupId>org.springframework.security</groupId>
			<artifactId>spring-security-web</artifactId>
			<version>3.2.3.RELEASE</version>
		</dependency>
	
		<!-- AspectJ -->
		<dependency>
			<groupId>org.aspectj</groupId>
			<artifactId>aspectjrt</artifactId>
			<version>${org.aspectj-version}</version>
		</dependency>	
		
		<!-- Logging -->
		<dependency>
			<groupId>org.slf4j</groupId>
			<artifactId>slf4j-api</artifactId>
			<version>${org.slf4j-version}</version>
		</dependency>
		<dependency>
			<groupId>org.slf4j</groupId>
			<artifactId>jcl-over-slf4j</artifactId>
			<version>${org.slf4j-version}</version>
			<scope>runtime</scope>
		</dependency>
		<dependency>
			<groupId>org.slf4j</groupId>
			<artifactId>slf4j-log4j12</artifactId>
			<version>${org.slf4j-version}</version>
			<scope>runtime</scope>
		</dependency>
		<dependency>
			<groupId>log4j</groupId>
			<artifactId>log4j</artifactId>
			<version>1.2.15</version>
			<exclusions>
				<exclusion>
					<groupId>javax.mail</groupId>
					<artifactId>mail</artifactId>
				</exclusion>
				<exclusion>
					<groupId>javax.jms</groupId>
					<artifactId>jms</artifactId>
				</exclusion>
				<exclusion>
					<groupId>com.sun.jdmk</groupId>
					<artifactId>jmxtools</artifactId>
				</exclusion>
				<exclusion>
					<groupId>com.sun.jmx</groupId>
					<artifactId>jmxri</artifactId>
				</exclusion>
			</exclusions>
			<scope>runtime</scope>
		</dependency>

		<!-- @Inject -->
		<dependency>
			<groupId>javax.inject</groupId>
			<artifactId>javax.inject</artifactId>
			<version>1</version>
		</dependency>
				
		<!-- Servlet -->
		<dependency>
			<groupId>javax.servlet</groupId>
			<artifactId>servlet-api</artifactId>
			<version>2.5</version>
			<scope>provided</scope>
		</dependency>
		<dependency>
			<groupId>javax.servlet.jsp</groupId>
			<artifactId>jsp-api</artifactId>
			<version>2.1</version>
			<scope>provided</scope>
		</dependency>
		<dependency>
			<groupId>javax.servlet</groupId>
			<artifactId>jstl</artifactId>
			<version>1.2</version>
		</dependency>
	
		<!-- Test -->
		<dependency>
			<groupId>junit</groupId>
			<artifactId>junit</artifactId>
			<version>4.7</version>
			<scope>test</scope>
		</dependency>
		<dependency>
			<groupId>org.springframework</groupId>
			<artifactId>spring-jdbc</artifactId>
			<version>4.0.2.RELEASE</version>
		</dependency>
	</dependencies>
  <build>
      <plugins>
          <plugin>
              <artifactId>maven-eclipse-plugin</artifactId>
              <version>2.9</version>
              <configuration>
                  <additionalProjectnatures>
                      <projectnature>org.springframework.ide.eclipse.core.springnature</projectnature>
                  </additionalProjectnatures>
                  <additionalBuildcommands>
                      <buildcommand>org.springframework.ide.eclipse.core.springbuilder</buildcommand>
                  </additionalBuildcommands>
                  <downloadSources>true</downloadSources>
                  <downloadJavadocs>true</downloadJavadocs>
              </configuration>
          </plugin>
          <plugin>
              <groupId>org.apache.maven.plugins</groupId>
              <artifactId>maven-compiler-plugin</artifactId>
              <version>2.5.1</version>
              <configuration>
                  <source>1.6</source>
                  <target>1.6</target>
                  <compilerArgument>-Xlint:all</compilerArgument>
                  <showWarnings>true</showWarnings>
                  <showDeprecation>true</showDeprecation>
              </configuration>
          </plugin>
          <plugin>
              <groupId>org.codehaus.mojo</groupId>
              <artifactId>exec-maven-plugin</artifactId>
              <version>1.2.1</version>
              <configuration>
                  <mainClass>org.test.int1.Main</mainClass>
              </configuration>
          </plugin>
      </plugins>
  </build>
</project>

我们已经为Spring Security包含了" spring-security-config"和" spring-security-web"依赖项。
除此之外,我们还有spring-jdbc的依赖,因为我们也将使用Spring JDBC认证。

其余的依赖关系与Spring MVC,日志记录,AOP等有关。

Spring Security示例部署描述符

我们的web.xml文件如下所示。

<?xml version="1.0" encoding="UTF-8"?>
<web-app version="2.5" xmlns="https://java.sun.com/xml/ns/javaee"
	xmlns:xsi="https://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="https://java.sun.com/xml/ns/javaee https://java.sun.com/xml/ns/javaee/web-app_2_5.xsd">

	<!-- Spring Security Configuration File -->
	<context-param>
		<param-name>contextConfigLocation</param-name>
		<param-value>/WEB-INF/spring/appServlet/spring-security.xml</param-value>
	</context-param>

	<!-- Creates the Spring Container shared by all Servlet and Filters -->
	<listener>
		<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
	</listener>
	<listener>
		<listener-class>org.springframework.security.web.session.HttpSessionEventPublisher</listener-class>
	</listener>
	
	<session-config>
		<session-timeout>15</session-timeout>
	</session-config>

	<!-- Spring Security Filter -->
	<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 MVC - START -->
	<servlet>
		<servlet-name>appServlet</servlet-name>
		<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
		<init-param>
			<param-name>contextConfigLocation</param-name>
			<param-value>
			/WEB-INF/spring/appServlet/servlet-context.xml
			</param-value>
		</init-param>
		<load-on-startup>1</load-on-startup>
	</servlet>

	<servlet-mapping>
		<servlet-name>appServlet</servlet-name>
		<url-pattern>/</url-pattern>
	</servlet-mapping>
	<!-- Spring MVC - END -->

</web-app>

contextConfigLocation是上下文参数,我们其中提供Spring Security Bean配置文件的名称。
ContextLoaderListener使用它在我们的应用程序中配置身份验证。

我们还添加了HttpSessionEventPublisher侦听器,以将会话创建/销毁的事件发布到Spring Root WebApplicationContext中。

我还将session-timeout设置为15分钟,这用于当用户不活动15分钟时自动超时。

DelegatingFilterProxy是定义的应用程序过滤器,用于拦截HTTP请求并执行与身份验证相关的任务。

DispatcherServlet servlet是Spring MVC应用程序的前端控制器。

UserDetailsService

如果我们想使用任何DAO类进行身份验证,则需要实现UserDetailsService接口。
配置DAO后,将使用loadUserByUsername()来验证用户。

package com.theitroad.spring.security.dao;

import java.util.Collection;
import java.util.List;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;

public class AppUserDetailsServiceDAO implements UserDetailsService {

	protected final Log logger = LogFactory.getLog(getClass());
	
	@Override
	public UserDetails loadUserByUsername(final String username)
			throws UsernameNotFoundException {
		
		logger.info("loadUserByUsername username="+username);
		
		if(!username.equals("hyman")){
			throw new UsernameNotFoundException(username + " not found");
		}
		
		//creating dummy user details, should do JDBC operations
		return new UserDetails() {
			
			private static final long serialVersionUID = 2059202961588104658L;

			@Override
			public boolean isEnabled() {
				return true;
			}
			
			@Override
			public boolean isCredentialsNonExpired() {
				return true;
			}
			
			@Override
			public boolean isAccountNonLocked() {
				return true;
			}
			
			@Override
			public boolean isAccountNonExpired() {
				return true;
			}
			
			@Override
			public String getUsername() {
				return username;
			}
			
			@Override
			public String getPassword() {
				return "hyman123";
			}
			
			@Override
			public Collection<? extends GrantedAuthority> getAuthorities() {
				List<SimpleGrantedAuthority> auths = new java.util.ArrayList<SimpleGrantedAuthority>();
				auths.add(new SimpleGrantedAuthority("Admin"));
				return auths;
			}
		};
	}

}

注意,我通过使用匿名内部类实现返回" UserDetails"实例。
理想情况下,我们应该为UserDetails提供一个实现类,该实现类还可以具有其他用户数据,例如emailID,用户名,地址等。

请注意,唯一有效的组合是用户名是" hyman",密码是" hyman123"。

Spring Security示例控制器类

这是我们的控制器类,它定义了我们可以访问的两个URI。

package com.theitroad.spring.controller;

import java.text.DateFormat;
import java.util.Date;
import java.util.Locale;

import javax.servlet.http.HttpServletRequest;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;

@Controller
public class HomeController {
	
	private static final Logger logger = LoggerFactory.getLogger(HomeController.class);
	
	@RequestMapping(value = "/home", method = RequestMethod.GET)
	public String home(Locale locale, Model model) {
		logger.info("Welcome home! The client locale is {}.", locale);
		Date date = new Date();
		DateFormat dateFormat = DateFormat.getDateTimeInstance(DateFormat.LONG, DateFormat.LONG, locale);
		
		String formattedDate = dateFormat.format(date);
		
		model.addAttribute("serverTime", formattedDate );
		
		return "home";
	}
	
	@RequestMapping(value = "/emp/get/{id}", method = RequestMethod.GET)
	public String getEmployee(Locale locale, Model model,@PathVariable("id") int id) {
		logger.info("Welcome user! Requested Emp ID is: "+id);
		Date date = new Date();
		DateFormat dateFormat = DateFormat.getDateTimeInstance(DateFormat.LONG, DateFormat.LONG, locale);
		
		String formattedDate = dateFormat.format(date);
		
		model.addAttribute("serverTime", formattedDate );
		model.addAttribute("id", id);
		model.addAttribute("name", "hyman");
		
		return "employee";
	}
	
	@RequestMapping(value="/login")
	public String login(HttpServletRequest request, Model model){
		return "login";
	}
	
	@RequestMapping(value="/logout")
	public String logout(){
		return "logout";
	}
	
	@RequestMapping(value="/denied")
	public String denied(){
		return "denied";
	}
}

在我们的示例中,我们将仅对URI"/emp/get/{id}"应用身份验证。
所有其他URI无需任何身份验证即可访问。
登录,注销和拒绝的URI用于在请求安全的URL时发送相应的响应页面。

Spring Security示例Bean配置文件

<?xml version="1.0" encoding="UTF-8"?>
<beans:beans xmlns="https://www.springframework.org/schema/mvc"
	xmlns:xsi="https://www.w3.org/2001/XMLSchema-instance"
	xmlns:beans="https://www.springframework.org/schema/beans"
	xmlns:context="https://www.springframework.org/schema/context"
	xsi:schemaLocation="https://www.springframework.org/schema/mvc https://www.springframework.org/schema/mvc/spring-mvc.xsd
		https://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd
		https://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd">

	<!-- DispatcherServlet Context: defines this servlet's request-processing infrastructure -->
	
	<!-- Enables the Spring MVC @Controller programming model -->
	<annotation-driven 

	<!-- Handles HTTP GET requests for /resources/** by efficiently serving up static resources in the ${webappRoot}/resources directory -->
	<resources mapping="/resources/**" location="/resources/" 

	<!-- Resolves views selected for rendering by @Controllers to .jsp resources in the /WEB-INF/views directory -->
	<beans:bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
		<beans:property name="prefix" value="/WEB-INF/views/" 
		<beans:property name="suffix" value=".jsp" 
	</beans:bean>
	
	<context:component-scan base-package="com.theitroad.spring.controller" 
	
</beans:beans>

我们的spring bean配置文件很简单,它的配置仅与Spring MVC应用程序有关。

Spring MVC安全配置

这是本教程最重要的部分,让我们看一下我们的文件。
我们将一一理解每个部分。

spring-security.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans:beans xmlns="https://www.springframework.org/schema/security"
	xmlns:beans="https://www.springframework.org/schema/beans" xmlns:xsi="https://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="https://www.springframework.org/schema/security https://www.springframework.org/schema/security/spring-security.xsd
			https://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd">

	<!-- Configuring RoleVoter bean to use custom access roles, by default roles 
		should be in the form ROLE_{XXX} -->
	<beans:bean id="roleVoter"
		class="org.springframework.security.access.vote.RoleVoter">
		<beans:property name="rolePrefix" value=""></beans:property>
	</beans:bean>

	<beans:bean id="accessDecisionManager"
		class="org.springframework.security.access.vote.AffirmativeBased">
		<beans:constructor-arg name="decisionVoters"
			ref="roleVoter" 
	</beans:bean>

	<http authentication-manager-ref="jdbc-auth"
		access-decision-manager-ref="accessDecisionManager">	
		<intercept-url pattern="/emp/**" access="Admin" 
		<form-login login-page="/login" authentication-failure-url="/denied"
			username-parameter="username" password-parameter="password"
			default-target-url="/home" 
		<logout invalidate-session="true" logout-success-url="/login"
			logout-url="/j_spring_security_logout" 
		<access-denied-handler error-page="/denied"
		<session-management invalid-session-url="/login">
			<concurrency-control max-sessions="1"
				expired-url="/login" 
		</session-management>
	</http>

	<authentication-manager id="in-memory-auth">
		<authentication-provider>
			<user-service>
				<user name="hyman" password="hyman123" authorities="Admin" 
			</user-service>
		</authentication-provider>
	</authentication-manager>

	<authentication-manager id="dao-auth">
		<authentication-provider user-service-ref="userDetailsService">
		</authentication-provider>
	</authentication-manager>

	<beans:bean id="userDetailsService"
		class="com.theitroad.spring.security.dao.AppUserDetailsServiceDAO" 

	<authentication-manager id="jdbc-auth">
		<authentication-provider>
			<jdbc-user-service data-source-ref="dataSource"
				users-by-username-query="select username,password,enabled from Employees where username = ?"
				authorities-by-username-query="select username,role from Roles where username = ?" 
		</authentication-provider>
	</authentication-manager>

	<!-- MySQL DB DataSource -->
	<beans:bean id="dataSource"
		class="org.springframework.jdbc.datasource.DriverManagerDataSource">

		<beans:property name="driverClassName" value="com.mysql.jdbc.Driver" 
		<beans:property name="url"
			value="jdbc:mysql://localhost:3306/TestDB" 
		<beans:property name="username" value="hyman" 
		<beans:property name="password" value="hyman123" 
	</beans:bean>

	<!-- If DataSource is configured in Tomcat Servlet Container -->
	<beans:bean id="dbDataSource"
		class="org.springframework.jndi.JndiObjectFactoryBean">
		<beans:property name="jndiName" value="java:comp/env/jdbc/MyLocalDB" 
	</beans:bean>
</beans:beans>

定义了accessDecisionManager bean,以便我们可以拥有自定义角色,默认情况下,所有角色都应以ROLE_开头,并且在roleVoter bean属性rolePrefix中覆盖此设置。

我们可以在spring安全配置中定义多个身份验证管理器。
我为内存身份验证定义了" in-memory-auth",为UserDetailsService DAO实现定义了" dao-auth",并且为JDBC身份验证定义了" jdbc-auth"。
对于JDBC身份验证,我为应用程序中定义的DataSource提供了配置,以及是否要使用在servlet容器中定义的JNDI资源。

httpauthentication-manager-ref用于定义将用于对用户进行身份验证的身份验证管理器。
目前已将其配置为使用基于JDBC的身份验证。

httpaccess-decision-manager-ref用于指定AccessDecisionManager实现的ID,该ID应该用于授权HTTP请求。

" intercept-url"用于定义URL模式和可以访问此页面的用户的权限。
例如,我们定义了URI"/emp/**"只能由具有"管理员"访问权限的用户访问。

form-login定义登录表单配置,我们可以提供用户名和密码参数名称。
" authentication-failure-url"用于定义身份验证失败页面的URL。
如果未指定登录失败URL,Spring Security将在/spring_security_login?login_error处自动创建一个失败登录URL,并创建一个相应的过滤器以在请求时呈现该登录失败URL。

" default-target-url"用于定义如果无法恢复用户的先前操作,则在成功身份验证后将重定向到的默认URL。
如果用户在未先请求触发身份验证的安全操作的情况下访问登录页面,通常会发生这种情况。
如果未指定,则默认为应用程序的根目录。

logout用于定义注销处理过滤器。
其中我们使会话无效,并在成功注销后将用户发送到登录页面。
logout-url用于定义用于注销操作的URL。

如果用户由于无权执行指定的操作而被拒绝访问,access-denied-handler定义全局错误页面。

session-management会将SessionManagementFilter过滤器添加到用于会话管理的过滤器堆栈中。

还有其他一些配置,但是我已经包括了我们使用的大多数重要配置。

Spring Security示例视图页面

在部署和测试应用程序之前,让我们快速浏览一下查看页面。

home.jsp

<%@ taglib uri="https://java.sun.com/jsp/jstl/core" prefix="c"%>
<%@ page session="false"%>
<html>
<head>
<title>Home</title>
</head>
<body>
	<h1>Hello world!</h1>

	<P>The time on the server is ${serverTime}.</P>
</body>
</html>

将为"/home" URI返回home.jsp,并且它不需要任何身份验证。

employee.jsp

<%@ taglib uri="https://java.sun.com/jsp/jstl/core" prefix="c"%>
<%@ page session="false"%>
<html>
<head>
<title>Get Employee Page</title>
</head>
<body>
	<h1>Employee Information</h1>
	<p>
		Employee ID:${id}<br> Employee Name:${name}<br>
	</p>
	<c:if test="${pageContext.request.userPrincipal.name != null}">
	Hi ${pageContext.request.userPrincipal.name}<br>
	
	<c:url var="logoutAction" value="/j_spring_security_logout"></c:url>
	
	<form action="${logoutAction}" method="post">
		<input type="submit" value="Logout" 
	</form>
	</c:if>
</body>
</html>

当我们访问需要身份验证的URI时,将返回此页面。
其中我提供了注销选项,以便用户可以注销并终止会话。
成功注销后,应按照配置将用户发送回登录页面。

login.jsp

<%@ taglib uri="https://java.sun.com/jsp/jstl/core" prefix="c"%>

<html>

<head>
<title>Login Page</title>
</head>
<body>
	<h3>Login with Username and Password</h3>
	<c:url var="loginUrl" value="/j_spring_security_check"></c:url>
	<form action="${loginUrl}" method="POST">
		<table>
			<tr>
				<td>User ID:</td>
				<td><input type='text' name='username' </td>
			</tr>
			<tr>
				<td>Password:</td>
				<td><input type='password' name='password' </td>
			</tr>
			<tr>
				<td colspan='2'><input name="submit" type="submit"
					value="Login" </td>
			</tr>
		</table>
	</form>
</body>
</html>

这里有几点要注意的要点。
第一个是登录URL为"/j_spring_security_check"。
这是默认的登录处理URL,就像注销URL一样。

另一个要点是用户名和密码的表单参数名称。
它们应该与spring安全配置中的配置相同。

logout.jsp

<html>
<head>
	<title>Logout Page</title>
</head>
<body>
<h2>
	Logout Successful!  
</h2>

</body>
</html>

denied.jsp

<html>
<head>
	<title>Access Denied</title>
</head>
<body>
<h1>
	Access Denied!  
</h1>

</body>
</html>

logout.jsp和否认.jsp页面很简单,但是我们可以根据用户详细信息在此处包含一些信息。

我们的Spring安全性示例应用程序已准备就绪,可以进行测试,请注意,对于JDBC身份验证,我使用的设置与先前的Spring Security Example相同。
因此,如果您直接降落其中则应该检查一下。