Spring 3之MVC的Security简单整合开发

   使用 spring mvc框架时,遇到登录问题, 原来项目登录地址包括:j_spring_security_check,是默认实现, 但要自己加一点自己的实现, 却不了解这个安全的工作过程,从网络上搜索到下面文章, 汇总了, 备忘。

     原文: http://sarin.iteye.com/blog/830831

    Spring的MVC模块是一种简洁的Web应用框架,实现了MVC模式来处理HTTP请求和响应。相比于Struts系列,SpringMVC的MVC 更加明显,将控制器和视图的定义完全分离,它们不需要在一个命名空间下了。它有Spring的全部优点,bean的配置更加舒服。而Spring 3的注解配置使得代码编写更加优雅。本例结合Spring MVC和Security框架进行小小整合,仅做功能说明,不详细探究其原理。
首先是建立项目,做一个简单的消息发布功能,代码结构如下,使用Maven可以很好的管理依赖:

采用了分层结构,但是没有使用到数据库操作,仅仅做个简短的说明,数据库操作用在后面Security框架验证用户时。下面来看看依赖关系,这样能对Spring的层次结构了解更加清晰:

先来看最基本的web部署描述文件web.xml,将用到的配置写好,Spring 3使用DispatcherServlet派发请求,而Security框架串接过滤器的机制来进行安全处理。配置很简单,如下即可,web请求使用.htm形式:

Xml代码

<?xml version="1.0" encoding="UTF-8"?>
<web-app version="2.5" xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee
http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd">
<context-param>
<param-value>
/WEB-INF/board-service.xml
/WEB-INF/board-security.xml
</param-value>
</context-param>
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<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>
<servlet>
<servlet-name>board</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>board</servlet-name>
<url-pattern>*.htm</url-pattern>
</servlet-mapping>
</web-app>


    下面是Servlet的配置文件,因为我们使用了注解,这里仅需对视图文件进行一下说明即可,而又配合后面的Security框架,在这里对 Security框架的方法拦截注解也声明了一下,这里说明一点,要拦截Controller的方法,必须将Security的声明和Servlet放在 一个文件内,否则拦截是没有作用的:

Xml代码

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:security="http://www.springframework.org/schema/security"
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.0.xsd

http://www.springframework.org/schema/context

http://www.springframework.org/schema/context/spring-context-3.0.xsd

http://www.springframework.org/schema/security

http://www.springframework.org/schema/security/spring-security-3.0.xsd">
<context:component-scan base-package="org.ourpioneer.board.web" />
<bean
class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="prefix" value="/WEB-INF/jsp/" />
<property name="suffix" value=".jsp" />
</bean>
<security:global-method-security
jsr250-annotations="enabled" secured-annotations="enabled" />
</beans>
其中对org.ourpioneer.board.web包进行组件扫描,就会发现我们注解声明的控制器了,下面是对视图解析的说明,我们把视图文件写在 /WEB-INF/jsp/下,后缀名为.jsp的文件就是视图文件,为什么把前缀和后缀都声明好了?因为程序里面我们直接写文件名就可以了,非常灵活, 它不关心是不是和请求路径是相同的。下面是对Controller方法拦截的Security框架的配置。
配置好Servlet相关内容,剩下就是Service内容了,这个很简单了,声明一个bean就是了,为了配合Security框架连接数据库验证用户身份,这里也配置一个数据源,使用Spring自己的数据源实现:

Xml代码
  1. <bean id="dataSource"        class="org.springframework.jdbc.datasource.DriverManagerDataSource">
  2. <property name="driverClassName" value="com.mysql.jdbc.Driver" />
  3.     <property name="url"
  4.         value="jdbc:mysql://localhost:3306/board" />
  5.     <property name="username" value="root" />
  6.     <property name="password" value="123" />
  7. </bean>
  8. <bean id="messageBoardService" class="org.ourpioneer.board.service.MessageBoardServiceImpl" />

配置好后,我们来看看程序代码,首先看看定义的领域对象Message,很简单的bean:

Java代码
  1. package org.ourpioneer.board.domain;
  2. public class Message {
  3.     private Long id;
  4.     private String author;
  5.     private String title;
  6.     private String body;
  7. //省略了getter和setter方法
  8. }

下面是Service,我们使用了实现和接口相分离的原则,方面后续在WebService中公开等,可能用不到,但这是一个良好的设计原则。接口内定义四个方法声明:

Java代码
  1. package org.ourpioneer.board.service;
  2. import java.util.List;
  3. import org.ourpioneer.board.domain.Message;
  4. public interface MessageBoardService {
  5.     public List<Message> listMessages();
  6.     public void postMessage(Message message);
  7.     public void deleteMeesage(Message message);
  8.     public Message findMessageById(Long messageId);
  9. }

下面是Service的实现类,就用List放置Message即可,这里我们对Service的方法也进行了安全拦截,这是更细粒度的拦截,后面会详细介绍,现在可以不管:

Java代码
  1. package org.ourpioneer.board.service;
  2. import java.util.ArrayList;
  3. import java.util.LinkedHashMap;
  4. import java.util.List;
  5. import java.util.Map;
  6. import org.ourpioneer.board.domain.Message;
  7. import org.springframework.security.access.annotation.Secured;
  8. public class MessageBoardServiceImpl implements MessageBoardService {
  9.     private Map<Long, Message> messages = new LinkedHashMap<Long, Message>();
  10.     //@Secured( { "ROLE_ADMIN", "IP_LOCAL_HOST" })
  11.     public synchronized void deleteMeesage(Message message) {
  12.         messages.remove(message.getId());
  13.     }
  14.     //@Secured( { "ROLE_USER", "ROLE_GUEST" })
  15.     public Message findMessageById(Long messageId) {
  16.         return messages.get(messageId);
  17.     }
  18.     //@Secured( { "ROLE_USER", "ROLE_GUEST" })
  19.     public List<Message> listMessages() {
  20.         return new ArrayList<Message>(messages.values());
  21.     }
  22.     //@Secured( { "ROLE_USER" })
  23.     public synchronized void postMessage(Message message) {
  24.         message.setId(System.currentTimeMillis());
  25.         messages.put(message.getId(), message);
  26.     }
  27. }

下面就该进入控制器部分了,我们一个一个来看:

Java代码
  1. package org.ourpioneer.board.web;
  2. import java.util.List;
  3. import org.ourpioneer.board.domain.Message;
  4. import org.ourpioneer.board.service.MessageBoardService;
  5. import org.springframework.beans.factory.annotation.Autowired;
  6. import org.springframework.security.access.annotation.Secured;
  7. import org.springframework.stereotype.Controller;
  8. import org.springframework.ui.Model;
  9. import org.springframework.web.bind.annotation.RequestMapping;
  10. import org.springframework.web.bind.annotation.RequestMethod;
  11. @Controller
  12. @RequestMapping("/messageList.htm")
  13. public class MessageListController {
  14.     @Autowired
  15.     private MessageBoardService messageBoardService;
  16.     @RequestMapping(method = RequestMethod.GET)
  17.     //@Secured( { "ROLE_USER" })
  18.     public String generateList(Model model) {
  19.         List<Message> messages = java.util.Collections.emptyList();
  20.         messages = messageBoardService.listMessages();
  21.         model.addAttribute("messages", messages);
  22.         return "messageList";
  23.     }
  24. }

对该类进行控制器注解声明,说明是Spring MVC中的控制器,下面是请求映射声明,处理/messageList.htm的请求,Service的注入采用自动装配,连set方法都不用了,下面是 对处理方法,可以看出,这是一个简单的POJO,连方法名都是我们自定义的,只需声明HTTP请求方法,就能找到方法了,而Model是传递数据给页面的 对象,把获取到的message列表放进去就行了,来看返回值,一个字符串,什么意思?就是JSP页面的名字,是不是很简单,MVC表现的淋漓尽致,这就 会找到页面了。
下面是发布消息的类:

Java代码
  1. package org.ourpioneer.board.web;
  2. import javax.servlet.http.HttpServletRequest;
  3. import org.ourpioneer.board.domain.Message;
  4. import org.ourpioneer.board.service.MessageBoardService;
  5. import org.springframework.beans.factory.annotation.Autowired;
  6. import org.springframework.security.access.annotation.Secured;
  7. import org.springframework.stereotype.Controller;
  8. import org.springframework.ui.Model;
  9. import org.springframework.validation.BindingResult;
  10. import org.springframework.web.bind.annotation.ModelAttribute;
  11. import org.springframework.web.bind.annotation.RequestMapping;
  12. import org.springframework.web.bind.annotation.RequestMethod;
  13. @Controller
  14. @RequestMapping("/messagePost.htm")
  15. public class MessagePostController {
  16.     @Autowired
  17.     private MessageBoardService messageBoardService;
  18.     @RequestMapping(method = RequestMethod.GET)
  19.     //@Secured( { "ROLE_USER" })
  20.     public String setupForm(Model model) {
  21.         Message message = new Message();
  22.         model.addAttribute("message", message);
  23.         return "messagePost";
  24.     }
  25.     @RequestMapping(method = RequestMethod.POST)
  26.     //@Secured( { "ROLE_USER" })
  27.     public String onSubmit(@ModelAttribute("message") Message message,
  28.             BindingResult result, HttpServletRequest request) {
  29.         message.setAuthor(request.getRemoteUser());
  30.         if (result.hasErrors()) {
  31.             return "messagePost";
  32.         } else {
  33.             messageBoardService.postMessage(message);
  34.             return "redirect:messageList.htm";
  35.         }
  36.     }
  37. }

GET方式是请求到这个页面,而POST方式是发布消息,最后是重定向,再到messageList.htm,就是这么简单的配置。要注意的是方法实现, 先看页面请求方法setupForm(Model model),参数上面已经解释了,是传递给页面的数据对象,里面放置了一个Message对象,做什么用的?肯定页面使用了,不过这是一个空对象,那么 自然想到要和表单属性进行绑定,等会看看页面就一清二楚了。下面是onSubmit方法,里面的参数都是我自己定义的,只要记住 BindingResult要和数据对象参数Message写在一起,后面的参数写想用的就行,那么我想用HttpServletRequest对象,就 写上去。真的很灵活。方法实现很简单,就不多说了。
最后是删除功能了,更简单了:

Java代码
  1. package org.ourpioneer.board.web;
  2. import org.ourpioneer.board.domain.Message;
  3. import org.ourpioneer.board.service.MessageBoardService;
  4. import org.springframework.beans.factory.annotation.Autowired;
  5. import org.springframework.security.access.annotation.Secured;
  6. import org.springframework.stereotype.Controller;
  7. import org.springframework.ui.Model;
  8. import org.springframework.web.bind.annotation.RequestMapping;
  9. import org.springframework.web.bind.annotation.RequestMethod;
  10. import org.springframework.web.bind.annotation.RequestParam;
  11. @Controller
  12. @RequestMapping("/messageDelete.htm")
  13. public class MessageDeleteController {
  14.     @Autowired
  15.     private MessageBoardService messageBoardService;
  16.     @RequestMapping(method = RequestMethod.GET)
  17.     //@Secured( { "ROLE_ADMIN" })
  18.     public String messageDelete(
  19.             @RequestParam(required = true, value = "messageId") Long messageId,
  20.             Model model) {
  21.         Message message = messageBoardService.findMessageById(messageId);
  22.         messageBoardService.deleteMeesage(message);
  23.         model.addAttribute("messages", messageBoardService.listMessages());
  24.         return "redirect:messageList.htm";
  25.     }
  26. }

只是权限设置为有管理员权限的才能删除,这里先不用。来看方法参数,我们必须要一个请求参数,是messageId,删除消息的标识符。下面就是操作了,很简单。
最后来看一下页面:

Html代码
  1. <%@taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
  2. <%@taglib prefix="security"
  3.     uri="http://www.springframework.org/security/tags"%>
  4. <html>
  5. <head>
  6. <title>Message List</title>
  7. </head>
  8. <body>
  9. <h2>Welcome! <security:authentication property="name" /></h2>
  10. <security:authentication property="authorities" var="authorities" />
  11. <ul>
  12.     <c:forEach items="${authorities}" var="authority">
  13.         <li>${authority.authority}</li>
  14.     </c:forEach>
  15. </ul>
  16. <hr />
  17. <c:forEach items="${messages}" var="message">
  18.     <table>
  19.         <security:authorize ifAllGranted="ROLE_ADMIN,ROLE_USER">
  20.             <tr>
  21.                 <td>Author</td>
  22.                 <td>${message.author}</td>
  23.             </tr>
  24.         </security:authorize>
  25.         <tr>
  26.             <td>Title</td>
  27.             <td>${message.title}</td>
  28.         </tr>
  29.         <tr>
  30.             <td>Body</td>
  31.             <td>${message.body}</td>
  32.         </tr>
  33.         <tr>
  34.             <td colspan="2"><a
  35.                 href="messageDelete.htm?messageId=${message.id}">Delete</a></td>
  36.         </tr>
  37.     </table>
  38.     <hr />
  39. </c:forEach>
  40. <a href="messagePost.htm">Post</a>
  41. <a href="<c:url value="/j_spring_security_logout" />">Logout</a>
  42. </body>
  43. </html>

列表页面有Security框架标签的使用,仅做MVC时可以先注释起来。这里使用了JSTL标签来遍历message列表,都很简单。
下面是发布消息的页面:

Html代码
  1. <%@taglib prefix="form" uri="http://www.springframework.org/tags/form"%>
  2. <html>
  3. <head>
  4. <title>Message Post</title>
  5. </head>
  6. <body>
  7. <form:form method="POST" modelAttribute="message">
  8.     <table>
  9.         <tr>
  10.             <td>Title</td>
  11.             <td><form:input path="title" /></td>
  12.         </tr>
  13.         <tr>
  14.             <td>Body</td>
  15.             <td><form:textarea path="body" /></td>
  16.         </tr>
  17.         <tr>
  18.             <td colspan="2"><input type="submit" value="Post" /></td>
  19.         </tr>
  20.     </table>
  21. </form:form>
  22. </body>
  23. </html>

前面说的数据绑定,这里就很容易看明白了吧。没有什么可以多解释的。
准备都做好后就是运行了,我们启动Jetty,来看看效果。关于Jetty的配置请参考作者之前对Maven和Jetty整合的文章,描述很详细。

因为我结合了Security框架,所以看到了我登录的身份列表,下面就是发布消息了,这就很简单了:

将结合Security框架进行简单安全的说明,是基于本例的。欢迎交流,希望对使用者有用。

    下面继续研究Spring MVC和Security的简单整合开发。但文本会略详细介绍Security的基本用法。
现在来说Security部分。Spring Security框架是Acegi Security的升级,这个框架就是利用了多重过滤的机制对请求进行处理,将符合要求的请求放行,不符合要求的请求阻止下来,这是最大的原理。下面先来看看简单的url过滤吧。
先写一个用于验证身份的登录页面:

Html代码
  1. <%@taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
  2. <html>
  3. <head>
  4. <title>Login</title>
  5. </head>
  6. <body>
  7. <c:if test="${not empty param.error}">
  8.     <font color="red">Login error.<br />
  9.     </font>
  10.     Reason:${sessionScope["SPRING_SECURITY_LAST_EXCEPTION"].message}
  11. </c:if>
  12. <form method="POST" action="<c:url value="/login"/>">
  13. <table>
  14.     <tr>
  15.         <td align="right">Username</td>
  16.         <td><input type="text" name="j_username" /></td>
  17.     </tr>
  18.     <tr>
  19.         <td align="right">Password</td>
  20.         <td><input type="password" name="j_password" /></td>
  21.     </tr>
  22.     <tr>
  23.         <td colspan="2" align="right"><input type="submit" value="Login" />
  24.         <input type="reset" value="Reset" /></td>
  25.     </tr>
  26. </table>
  27. </form>
  28. </body>
  29. </html>

做一些说明,使用Spring Security时,默认的登录验证地址是j_spring_security_check,验证的用户名是j_username,密码是 j_password,对于用户名和密码我们不需要修改,使用其默认值即可,而验证路径通常我们想使用自定义地址,这就需要在security中进行配 置,后面会看到,这里还会看到如果验证失败,会把失败信息打印出来,就是JSTL的c:if段的作用。下面来看最基本的Security框架作用,拦截 URL请求。在board-security.xml配置如下:

Xml代码
  1. <?xml version="1.0" encoding="UTF-8"?>
  2. <beans xmlns="http://www.springframework.org/schema/beans"
  3.     xmlns:security="http://www.springframework.org/schema/security"
  4.     xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  5.     xsi:schemaLocation="http://www.springframework.org/schema/beans
  6. http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
  7. http://www.springframework.org/schema/security
  8. http://www.springframework.org/schema/security/spring-security-3.0.xsd">
  9.     <security:http auto-config="true">
  10. <security:intercept-url pattern="/messageList.htm"
  11.                     access="ROLE_ADMIN,ROLE_USER,ROLE_GUEST" />
  12.         <security:intercept-url pattern="/messagePost.htm"
  13.                     access="ROLE_ADMIN,ROLE_USER" />
  14.         <security:intercept-url pattern="/messageDelete.htm"
  15.                     access="ROLE_ADMIN" />
  16.         <security:anonymous username="guest"
  17.             granted-authority="ROLE_GUEST" />
  18.     </security:http>
  19.     <security:authentication-manager>
  20.         <security:authentication-provider>
  21.             <security:user-service>
  22.                 <security:user name="admin" authorities="ROLE_ADMIN,ROLE_USER" password="secret" />
  23.                 <security:user name="user1" authorities="ROLE_USER"
  24.                                 password="1111" />
  25. </security:user-service>
  26.         </security:authentication-provider>
  27.     </security:authentication-manager>
  28. </beans>

配置文件中首先是对http请求的拦截,这里使用了自动配置auto-config,那么当请求到达时,Security框架会让我们进行身份验证,我们 拦截的url模式已经在其中配置出来,三个请求分别对应不同的权限,而且messageList.htm还开放了匿名访问功能,要提供匿名访问,就要配 置<anonymous>,这里我们配置匿名用户名为guest,角色是ROLE_GUEST,这里的角色都是ROLE_开头,是 Spring Security框架默认使用的,我们不用去更改,这也很清楚。首先我们启动应用,来访问唯一的匿名功能,之后我们看到如下效果:

可以看到,现在的角色是ROLE_GUEST,那么就直接看到,没有验证身份,若我们要发布消息呢,点击Post链接,看看效果:

要求身份验证了,这就说明对url的拦截起作用了,想发布消息,权限不够了,要验证身份了,注意这里这个页面并不是我们前面写的那个页面,而是 Security框架的默认验证页面,为什么没有使用我们所写的页面呢?因为我们还没有配置它啊,当然不会被识别到了。我们来看看默认的页面源码是什么样 子的:

Html代码
  1. <html><head><title>Login Page</title></head><body onload='document.f.j_username.focus();'>
  2. <h3>Login with Username and Password</h3><form name='f' action='/j_spring_security_check' method='POST'>
  3.  <table>
  4.     <tr><td>User:</td><td><input type='text' name='j_username' value=''></td></tr>
  5.     <tr><td>Password:</td><td><input type='password' name='j_password'/></td></tr>
  6.     <tr><td colspan='2'><input name="submit" type="submit"/></td></tr>
  7.     <tr><td colspan='2'><input name="reset" type="reset"/></td></tr>
  8.   </table>
  9. </form></body></html>

可以看到这里的默认请求路径就是/j_spring_security_check了,不过这里我们已经可以使用我们配置的用户来登录了,之前在配置文件 中的admin和user1,它们拥有的权限不同,那么我们使用user1登录,来发布消息。验证通过,出现消息输入页面:

下面发布消息,之后能看到消息的列表了,这对ROLE_USER的角色都是可以查看的。

没有把Author的信息打印出来,为什么?我们在这里对这个自动进行了限制,来看一下页面是怎么写的:

Xml代码
  1. <security:authorize ifAllGranted="ROLE_ADMIN,ROLE_USER">
  2.     <tr>
  3.         <td>Author</td>
  4.         <td>${message.author}</td>
  5.     </tr>
  6. </security:authorize>

这里说的是拥有ROLE_ADMIN和ROLE_USER两种角色才能显示author信息,显然我们权限不够了,当然把这里修改为 ifAnyGranted=”ROLE_ADMIN,ROLE_USER”就可以显示出来了,All和Any的区别嘛,很容易理解。还有一个属性是 ifNotGranted,不用说你也会明白它是什么意思了,我们现在修改为ifAnyGranted=”ROLE_ADMIN,ROLE_USER”, 刷新页面,就会看到如下内容了:

其实这已经是在扩展Security框架的视图功能了,就是这么使用的,如果想了解security框架标签库其它标签,那么去参考官方文档吧,因为你已经知道该怎么去套用了。
该试试删除功能了,当前用户角色是ROLE_USER,想删除肯定是不可以的了,那么会是怎么样的效果呢,点击Delete链接,看一下吧:

非常不幸,被拦截下来了,HTTP 403表示没有权限,那么就对了,Security框架起作用了,这就是我们想要的效果了。
Security框架的基本URL拦截到此就说完了,是不是很简单?下面就来定制一些操作吧,我们既然编写了自定义登录页面,得用上吧,还有Logout退出功能没用呢。下面就对这基本的配置进行第一次扩展,我们这样做:

Xml代码
  1. <security:http>
  2.     <security:intercept-url pattern="/messageList.htm"
  3.                     access="ROLE_ADMIN,ROLE_USER,ROLE_GUEST" />
  4.     <security:intercept-url pattern="/messagePost.htm"
  5.                     access="ROLE_ADMIN,ROLE_USER" />
  6.     <security:intercept-url pattern="/messageDelete.htm"
  7.                     access="ROLE_ADMIN" />
  8.     <security:form-login login-page="/login.jsp"
  9. login-processing-url="/login" default-target-url="/messageList.htm"
  10.             authentication-failure-url="/login.jsp?error=true" />
  11.     <security:logout logout-success-url="/login.jsp" />
  12. </security:http>

首先去掉auto-config,因为要定制,不让Security按它默认的执行。那么登录验证就配置吧,login-page属性配置的是登录页面, 就是我们前面所写的,login-processing-url就是我们处理登录逻辑的请求地址,默认的是 j_spring_security_check,前面也说过了,default_target_url就是默认的登录成功转向的目标地址,这里是消息列 表页面。最后一个属性是authentication-failure-url,很明白了,就是验证失败转向的页面,这里我们附加一个参数error,页 面里面也有体现,就是用它来控制失败信息的打印的。下面一个是配置退出,logout-success-url就是退出后转向的页面,这里是到登录页面, 没错,退出后回到登录页面。下面来看看效果吧,修改完毕重启Jetty:

由于去掉了匿名访问,那么直接请求messageList.htm就会为我们跳转到登录页面了,进行身份验证,此时我们输入一个错误的信息,看看能捕捉到什么:

验证失败错误会出现Bad credentials,这里不判断是用户不存在还是密码错误,统一是登录凭据错误。输入正确的信息就可以重复上述操作了。使用admin登录成功,会出现:

至此基本的Security拦截操作已经说完了,是不是很简单呢。当然这是测试的,真实应用中我们的用户不可能这么配置,因为都是存放在数据库中的,那么 Security能不能支持数据库用户验证呢?答案是肯定的。只是需要一些扩展配置,这里Security整合了一些数据库验证的操作,要符合 Security的验证模式,那么要么我们重新设计数据库,要么在原有基础之上来修改一下数据库设计。这里我们先看一下Security框架默认支持的数 据库设计吧,就是它默认SQL查询语句所支持的内容。

这两个表明是默认的,这么写Security会自己识别出来,不用我们书写SQL语句了。先来看看表设计吧,就这些信息就够Security进行验证了。

Sql代码
  1. CREATE TABLE `users` (
  2.   `USERNAME` varchar(10) NOT NULL,
  3.   `PASSWORDvarchar(32) NOT NULL,
  4.   `ENABLED` tinyint(1) NOT NULL,
  5.   PRIMARY KEY (`USERNAME`)
  6. ) ENGINE=InnoDB DEFAULT CHARSET=utf8
  7. CREATE TABLE `authorities` (
  8.   `USERNAME` varchar(10) NOT NULL,
  9.   `AUTHORITY` varchar(10) NOT NULL,
  10.   KEY `FK_USERNAME_AUTHORITY` (`USERNAME`),
  11.   CONSTRAINT `FK_USERNAME_AUTHORITY` FOREIGN KEY (`USERNAME`) REFERENCES `users` (`USERNAME`) ON DELETE NO ACTION ON UPDATE NO ACTION
  12. ) ENGINE=InnoDB DEFAULT CHARSET=utf8

两个表之间有一个外键的关联,是用户名关联,而且我们还进行了md5密码扩展,这也要在Security框架进行配置,在表中插入一些信息,就可以进行数据库验证了,此时Security框架的配置如下,修改认证管理器:

Xml代码
  1. <security:authentication-manager>
  2.     <security:authentication-provider>
  3.         <security:password-encoder ref="md5Encoder" />
  4.         <security:jdbc-user-service data-source-ref="dataSource" />
  5.     </security:authentication-provider>
  6. </security:authentication-manager>

这里我们配置了jdbc数据源和密码编码器,因为连MD5加密方式也是我们自定义的,这样安全系数更高。要使用自定义的加密器,别忘了编写加密器的bean。

Xml代码
  1. <bean id="md5Encoder" class="org.ourpioneer.board.util.MD5Encoder" />

加密器类需要实现PasswordEncoder接口,然后编写我们自己的加密方案,加密器很简单,如下设计:

Java代码
  1. package org.ourpioneer.board.util;
  2. import org.springframework.dao.DataAccessException;
  3. import org.springframework.security.authentication.encoding.PasswordEncoder;
  4. public class MD5Encoder implements PasswordEncoder {
  5.     public String encodePassword(String origPwd, Object salt)
  6.             throws DataAccessException {
  7.         return MD5.getMD5ofStr(origPwd);
  8.     }
  9.     public boolean isPasswordValid(String encPwd, String origPwd, Object salt)
  10.             throws DataAccessException {
  11.         return encPwd.equals(encodePassword(origPwd, salt));
  12.     }
  13. }

其中使用到的MD5加密类为:

Java代码
  1. package org.ourpioneer.board.util;
  2. import java.security.MessageDigest;
  3. /**
  4.  * 标准MD5加密方法,使用java类库的security包的MessageDigest类处理 <BR>
  5.  * 也可变为非标准MD5,请修改下面的移位算法
  6.  * 
  7.  * @author Nanlei
  8.  * 
  9.  */
  10. public class MD5 {
  11.     /**
  12.      * 获得MD5加密密码的方法
  13.      */
  14.     public static String getMD5ofStr(String origString) {
  15.         String origMD5 = null;
  16.         try {
  17.             MessageDigest md5 = MessageDigest.getInstance("MD5");
  18.             byte[] result = md5.digest(origString.getBytes());
  19.             origMD5 = byteArray2HexStr(result);
  20.         } catch (Exception e) {
  21.             e.printStackTrace();
  22.         }
  23.         return origMD5;
  24.     }
  25.     /**
  26.      * 处理字节数组得到MD5密码的方法
  27.      */
  28.     private static String byteArray2HexStr(byte[] bs) {
  29.         StringBuffer sb = new StringBuffer();
  30.         for (byte b : bs) {
  31.             sb.append(byte2HexStr(b));
  32.         }
  33.         return sb.toString();
  34.     }
  35.     /**
  36.      * 字节标准移位转十六进制方法
  37.      */
  38.     private static String byte2HexStr(byte b) {
  39.         String hexStr = null;
  40.         int n = b;
  41.         if (n < 0) {
  42.             // 若需要自定义加密,请修改这个移位算法即可
  43.             n = b & 0x7F + 128;
  44.         }
  45.         hexStr = Integer.toHexString(n / 16) + Integer.toHexString(n % 16);
  46.         return hexStr.toUpperCase();
  47.     }
  48.     /**
  49.      * 提供一个MD5多次加密方法
  50.      */
  51.     public static String getMD5ofStr(String origString, int times) {
  52.         String md5 = getMD5ofStr(origString);
  53.         for (int i = 0; i < times - 1; i++) {
  54.             md5 = getMD5ofStr(md5);
  55.         }
  56.         return getMD5ofStr(md5);
  57.     }
  58.     /**
  59.      * 密码验证方法
  60.      */
  61.     public static boolean verifyPassword(String inputStr, String MD5Code) {
  62.         return getMD5ofStr(inputStr).equals(MD5Code);
  63.     }
  64.     /**
  65.      * 多次加密时的密码验证方法
  66.      */
  67.     public static boolean verifyPassword(String inputStr, String MD5Code,
  68.             int times) {
  69.         return getMD5ofStr(inputStr, times).equals(MD5Code);
  70.     }
  71.     /**
  72.      * 提供一个测试的主函数
  73.      */
  74.     public static void main(String[] args) {
  75.         System.out.println("123:" + getMD5ofStr("123"));
  76.         System.out.println("123456789:" + getMD5ofStr("123456789"));
  77.         System.out.println("pioneer:" + getMD5ofStr("pioneer"));
  78.         System.out.println("123:" + getMD5ofStr("123"4));
  79.     }
  80. }

加密工作已经准备好,之前配置的数据源是:

Xml代码
  1. <bean id="dataSource"
  2.     class="org.springframework.jdbc.datasource.DriverManagerDataSource">
  3.     <property name="driverClassName" value="com.mysql.jdbc.Driver" />
  4.         <property name="url"
  5.             value="jdbc:mysql://localhost:3306/board" />
  6.         <property name="username" value="root" />
  7.         <property name="password" value="123" />
  8.     </bean>

别忘了加入JDBC的驱动程序,之后我们就可以使用数据库用户验证了,剩下的步骤就和前面是一样的了。
至此我们已经了解了Security对标准设置的数据库验证的操作了,下一篇将从非标准的数据库验证开始继续介绍Security框架。欢迎交流,希望对使用者有用。

-----------------------------
 再次继续深入研究Security框架。
Security对数据库验证用户有两种方式,上文提到的是它默认支持的数据库表结构,但基本上用于实际是不现实的,因为我们的数据库都有自己的业务逻 辑,所以现在来看看怎么在我们自己的数据库上进行Security框架的用户验证整合,这里给出一个比较通用的数据库权限设计结构:

假设我们的数据表名称为b_user和b_userrole,它们的结构如下:

Sql代码
  1. CREATE TABLE `b_user` (
  2.   `ID` int(11) NOT NULL AUTO_INCREMENT,
  3.   `USERNAME` varchar(20) NOT NULL,
  4.   `PASSWORDvarchar(32) NOT NULL,
  5.   PRIMARY KEY (`ID`)
  6. ) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8
  7. CREATE TABLE `b_userrole` (
  8.   `ID` int(11) NOT NULL,
  9.   `USERID` int(11) NOT NULL,
  10.   `ROLE` varchar(15) NOT NULL,
  11.   PRIMARY KEY (`ID`),
  12.   KEY `FK_USERID_USERROLE` (`USERID`),
  13.   CONSTRAINT `FK_USERID_USERROLE` FOREIGN KEY (`USERID`) REFERENCES `b_user` (`ID`) ON DELETE NO ACTION ON UPDATE NO ACTION
  14. ) ENGINE=InnoDB DEFAULT CHARSET=utf8

那么表名,字段名和结构都和Security框架默认的不匹配,只好通过SQL语句来让Security框架识别了,在配置文件的数据库验证部分,我们可以这么来写:

Xml代码
  1. <security:authentication-manager>
  2.     <security:authentication-provider>
  3.         <security:password-encoder ref="md5Encoder" />
  4.         <security:jdbc-user-service
  5.             data-source-ref="dataSource"
  6.             users-by-username-query="select USERNAME,PASSWORD,'true' as ENABLED from b_user where USERNAME=?"
  7.             authorities-by-username-query="select u.USERNAME,ur.ROLE as AUTHORITIES from b_user u,b_userrole ur where u.USERNAME=? and u.ID=ur.USERID" />
  8.     </security:authentication-provider>
  9. </security:authentication-manager>

在jdbc-user-service中,我们启用了两个属性,其中放置的是SQL语句,就是我们自定义的用户验证方式,将我们的数据库设计和Security框架相匹配,这里的角色一定是在拦截url标签中配置过的,否则Security框架不能识别用户身份。
启动应用程序,发现这和原来的验证效果是一样的,这就是自定义的数据库验证方式了。也非常简单,就是用SQL语句查询结果匹配Security框架,不过这可能要和自己应用的数据库设计做出调整,尽量做到最小调整。
这里补充一点用户验证方式的配置,我们已经使用了在配置文件里配置用户和数据库验证,一种是支持为数不多的用户,一种是支持数据库大量查询的。对于前者, 将配置信息写在XML文件中,和Security框架的配置信息粘在一起,不利于维护。其实Security框架也支持属性文件的配置,我们可以这么来 写:

Xml代码
  1. <security:authentication-manager>
  2.     <security:authentication-provider>
  3.         <security:user-service properties="/WEB-INF/users.properties"/>
  4.     </security:authentication-provider>
  5. </security:authentication-manager>

这里把用户信息都写在了users.properties里,我们来看这个属性配置文件:

Java代码
  1. admin=123,ROLE_ADMIN,ROLE_USER
  2. user1=123,ROLE_USER
  3. user2=123,enabled,ROLE_USER

这里面名/值对的形式排列的,值的字段比较多,我们来逐个解释。admin/user1是用户名,不用多说,等号后面第一位是密码,这里没有加密。第二位 是状态,这是可选的,默认是enabled,第三位以后就是用户所拥有的角色了,这么使用和前面的效果也是相同的。
用户验证部分基本就这么多内容,这里没有涉及到LDAP相关部分。下面是访问控制的说明,访问控制是Security框架的另一大特性,可以对其进行自定义的扩展,设计符合我们业务逻辑的控制。这比URL拦截又深入了一步,可以过滤的东西又多了。
设计到访问控制,要引入一个概念,谁来决定能否访问,从而进行控制。Security框架中的访问控制管理有三种方案:至少有一个同意访问,全部同意访 问,全部弃权或都同意访问(也就是没有拒绝的)。如何同意?投票产生!Security框架一个可配置的元素就出来了,那就是投票器了。和现实的投票一 样,分同意,反对和弃权三类。
下面我们应用第一类访问控制管理:至少有一个投票器同意访问,在配置文件中这么来设置:

Xml代码
  1. <bean id="accessDecisionManager"
  2.     class="org.springframework.security.access.vote.AffirmativeBased">
  3.     <property name="decisionVoters">
  4.         <list>
  5.             <bean class="org.springframework.security.access.vote.RoleVoter" />
  6.             <bean class="org.springframework.security.access.vote.AuthenticatedVoter" />
  7.         </list>
  8.     </property>
  9. </bean>

上面是默认需要的认证投票,下面就是我们定制的内容了,用来满足特定需要。在消息发布应用中,有这样一个需求,在服务器上登录的用户,给可以删除消息的权 限,也就是说不用管理员账户登录,也能删除。那么我们就需要对访问进行控制。在服务器本身登录的用户的IP应该是本地地址127.0.0.1,那么只要 IP是它的允许删除,我们来自定义一个投票器来进行投票:

Java代码
  1. package org.ourpioneer.board.security;
  2. import java.util.Collection;
  3. import org.springframework.security.access.AccessDecisionVoter;
  4. import org.springframework.security.access.ConfigAttribute;
  5. import org.springframework.security.core.Authentication;
  6. import org.springframework.security.web.authentication.WebAuthenticationDetails;
  7. public class IPAddressVoter implements AccessDecisionVoter {
  8.     public static final String IP_PREFIX = "IP_";
  9.     public static final String IP_LOCAL_HOST = "IP_LOCAL_HOST";
  10.     public boolean supports(ConfigAttribute attribute) {
  11.         return attribute.getAttribute() != null
  12.                 && attribute.getAttribute().startsWith(IP_PREFIX);
  13.     }
  14.     public boolean supports(Class<?> clazz) {
  15.         return true;
  16.     }
  17.     public int vote(Authentication authentication, Object object,
  18.             Collection<ConfigAttribute> attributes) {
  19.         if (!(authentication.getDetails() instanceof WebAuthenticationDetails)) {
  20.             return ACCESS_DENIED;
  21.         }
  22.         WebAuthenticationDetails details = (WebAuthenticationDetails) authentication
  23.                 .getDetails();
  24.         String address = details.getRemoteAddress();
  25.         int result = ACCESS_ABSTAIN;
  26.         for (ConfigAttribute config : attributes) {
  27.             result = ACCESS_DENIED;
  28.             if (IP_LOCAL_HOST.equals(config.getAttribute())) {
  29.                 if (address.equals("127.0.0.1")
  30.                         || address.equals("0:0:0:0:0:0:0:1")) {
  31.                     return ACCESS_GRANTED;
  32.                 }
  33.             }
  34.         }
  35.         return result;
  36.     }
  37. }

IP地址投票器仅处理属性开头是IP的访问,而支持访问的只能是IP_LOCAL_HOST访问属性。如果访问者的IP是127.0.0.1或者0:0:0:0:0:0:0:1的可以访问,其余的拒绝访问。在配置文件的投票器list中再加入这个类:

Xml代码
  1. <bean class="org.ourpioneer.board.security.IPAddressVoter" />

之后还要在URL拦截属性中修改配置IP_LOCAL_HOST属性的访问权限:

Xml代码
  1. <security:intercept-url pattern="/messageDelete.htm" access="ROLE_ADMIN,IP_LOCAL_HOST" />

而且在http中还要配置访问决定管理器,否则是不能识别到IP_LOCAL_HOST的:

Xml代码
  1. <security:http access-decision-manager-ref="accessDecisionManager">

此时,在本地用user1用户登录,也具有了删除权限,可以删除文章了。这就是投票器的简单应用了。下面是方法调用安全,这是非常细粒度的安全控制,可以 作用于类的方法,那么也就是说,对一块业务逻辑有权限的用户组,可能允许你能添加而不能删除,他能修改而不能添加和删除,这都是可以实现的,因为这已经细 化到了方法之上了,一个类的某一个方法给你授权访问,其余方法就访问不到,细化到一个功能点上的访问,安全性有很大的提升。先看看对控制器方法的安全访 问,这个配置相对简单,在配置文件中,把安全配置文件和controller的声明放在一起:

Xml代码
  1. <context:component-scan base-package="org.ourpioneer.board.web" />
  2. <security:global-method-security
  3.         jsr250-annotations="enabled" secured-annotations="enabled" />

这样才能对controller的方法进行控制。不过对方法实行安全控制之后,就没有必要对URL进行拦截了,http配置中的url拦截就都可以去掉了,仅留下登录和退出的就可以了:

Xml代码
  1. <security:http access-decision-manager-ref="accessDecisionManager">
  2.     <security:form-login login-page="/login.jsp"
  3.         login-processing-url="/login" default-target-url="/messageList.htm"
  4.             authentication-failure-url="/login.jsp?error=true" />
  5.     <security:logout logout-success-url="/login.jsp" />
  6. </security:http>

虽然这里加入了access-decision-manager-ref="accessDecisionManager",但是对方法的安全不是这里做 的,所以这样的话使用user1登录就没有对消息的删除权限了,那么怎么能恢复呢?很简单,在global-method-security中加入它就可 以了。这就完成了对控制器方法的配置,那么因为前面都是注解实现的,所以在方法上配置注解就行了,前面代码很全,这里给出一个示例:

Java代码
  1. @RequestMapping(method = RequestMethod.GET)
  2. @Secured( { "ROLE_ADMIN""IP_LOCAL_HOST" })
  3. public String messageDelete(
  4.     @RequestParam(required = true, value = "messageId") Long messageId,
  5.             Model model) {
  6.     Message message = messageBoardService.findMessageById(messageId);
  7.     messageBoardService.deleteMeesage(message);
  8.     model.addAttribute("messages", messageBoardService.listMessages());
  9.     return "redirect:messageList.htm";
  10. }

只要在注解方法上表明可访问的权限就能实现拦截了。当然在Service上实现拦截同理可得,只是需要注意一下注解声明的所在配置文件,否则可能无效,这 确实有点不爽。方法拦截除了注解,还有嵌入配置方式和切点配置方式,这两种都是常规做法,参考官方文档就可以了。
最后一点是V层的拦截,这在前面已经提到了,使用的是Security框架的标签库实现的。可用的标签和属性,直接参考官方文档即可,使用也很方便,这里就不过多说明了。
Spring 3的MVC和Security框架的简单整合就介绍完了,没有过深内容,都是基础应用,如果想深入了解,官方文档是最佳的学习资料。本文的示例代码请直接下载,需要Maven环境来支持,关于Maven环境的配置,可以参考Maven构建Java Web开发环境的介绍。
欢迎交流,希望对使用者有用。

发表评论