下午好
本文将介绍使用Spring Boot和Spring Security创建一个简单的Web应用程序。 该应用程序将实现新用户的注册和授权,并根据用户的角色来限制对网站页面的访问。
本文的主要目的是向您展示如何限制具有不同角色的用户对网站各个页面的访问。
该应用程序是什么?
具有以下页面的网站:
- 所有用户均可访问的页面:主页,注册和登录;
- 注册用户可用的页面:新闻;
- 管理员页面可用。
我们将使用什么
- JDK 8+;
- Intellij Idea;
- Spring(Spring Boot,Spring MVC,Spring Security);
- 冬眠
- JSP
- PostgreSQL的
目录内容
- 使用的主要注释的描述。
- 在IDE中创建一个新项目。
- 创建一个项目结构(包)。
- 添加实体,控制器,服务,存储库和视图。
- 应用程序启动。
1.使用的主要注释的描述
控制器是MVC应用程序中使用的一种特殊类型的类。 它看起来像一个常规HttpServlet servlet,可与HttpServletRequest和HttpServletResponse对象一起使用,但具有Spring Framework的高级功能。
存储库 -指示该类用于指定列表
搜索,检索和存储数据所需的工作。 注释可用于实现DAO模板。
服务 -表示该类是用于实现业务逻辑的服务。
配置 -此注释用于定义Bean组件的类。
自动
连线 -注解可让您自动设置字段的值。 此注释的功能是,我们不必担心如何最好地将另一个Bean的副本传递给Bean。 Spring本身将找到所需的Bean,并将其值替换为带有注释标记的属性。
有关Spring Security的一些信息
最基本的对象是
SecurityContextHolder 。 它存储有关应用程序当前安全性上下文的信息,其中包括有关使用该应用程序的用户(主要)的详细信息。 Spring Security使用
Authentication对象,即授权的会话用户。
“用户”只是一个对象。 在大多数情况下,
强制转换为
UserDetails类。 可以将
UserDetails视为用户数据库之间的适配器,以及Spring Security在
SecurityContextHolder内部需要的适配器。
要创建
UserDetails ,请使用
UserDetailsService接口,并使用一个方法:
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException
2.在IDE中创建一个新项目
我们将使用Maven构建系统。
GroupId表示发布项目的公司的唯一标识符(或您的个人域名)。
ArtefactId只是我们项目的名称。

项目创建完成后,pom.xml文件
打开 ,Idea将提供启用自动导入的功能-不要拒绝。 该文件将包含项目中使用的所有依赖项(库)。

3.创建项目结构(包)
立即继续创建软件包。 如下所示,应显示的项目结构。

现在简要介绍一下每个包中将存储的内容:
- src \ main \ java \ com \ boots \配置 -具有用于MVC(MvcConfig)和安全性(WebSecurityConfig)的配置的类;
- src \ main \ java \ com \ boots \ controller-带有控制器的类;
- src \ main \ java \ com \ boots \实体 -具有模型的类;
- src \ main \ java \ com \ boots \ repository-仓库接口;
- src \ main \ java \ com \ boots \ service-带有模型服务的类;
- src \ main \ webapp \ resources-静态对象:js,css,img;
- src \ main \ webapp \ WEB-INF \ jsp -.jsp文件形式的表示形式。
考虑
pom.xml文件。 在此文件中,您需要使用
父标签指定指向父文件的链接,即 父级的所有属性和依赖项将添加到此子文件中。
<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.1.9.RELEASE</version> </parent>
接下来,添加Spring模块,PostgreSQL数据库驱动程序,Tomcat服务器,JSTL的依赖关系。
<properties> <java.version>1.8</java.version> </properties>
默认情况下,maven将使用Java 1.6的旧版本,要解决此问题,我们将明确指定版本。
<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <dependency> <groupId>org.postgresql</groupId> <artifactId>postgresql</artifactId> <version>42.2.8</version> <scope>runtime</scope> </dependency> <dependency> <groupId>javax.servlet</groupId> <artifactId>jstl</artifactId> <version>1.2</version> </dependency> <dependency> <groupId>org.apache.tomcat.embed</groupId> <artifactId>tomcat-embed-jasper</artifactId> <version>9.0.27</version> </dependency> <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-taglibs</artifactId> <version>5.2.0.RELEASE</version> </dependency> </dependencies>
我们还添加了一个插件,可让您打包jar或war存档并“就地”运行它们:
<build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build>
完整的pom.xml <?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>ark</groupId> <artifactId>spring</artifactId> <version>1.0-SNAPSHOT</version> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.1.6.RELEASE</version> </parent> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <dependency> <groupId>org.postgresql</groupId> <artifactId>postgresql</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>javax.servlet</groupId> <artifactId>jstl</artifactId> <version>1.2</version> </dependency> <dependency> <groupId>org.apache.tomcat.embed</groupId> <artifactId>tomcat-embed-jasper</artifactId> <version>9.0.27</version> </dependency> <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-taglibs</artifactId> <version>5.2.0.RELEASE</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-devtools</artifactId> </dependency> </dependencies> <properties> <java.version>1.8</java.version> </properties> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>
填写
application.properties文件。 前三行包含用于连接数据库的数据(数据库名称为“ spring”,登录名和密码)。 最后两行指示.jsp文件的路径:
spring.datasource.url=jdbc:postgresql://localhost/spring spring.datasource.username=postgres spring.datasource.password=password spring.jpa.show-sql=true spring.jpa.generate-ddl=false spring.jpa.hibernate.ddl-auto=update spring.jpa.properties.hibernate.jdbc.lob.non_contextual_creation=true spring.mvc.view.prefix = /WEB-INF/jsp/ spring.mvc.view.suffix = .jsp
spring.jpa.show-sql属性向控制台显示数据库查询的主体。
spring.jpa.hibernate.ddl-auto允许您基于我们的模型设置策略来创建数据库,并具有不同的值(无,创建,更新等)。 在这种情况下,
update表示数据库表和字段将根据我们的模型创建并随其更改。
展望未来,我们只需要创建一个名称为
spring的数据库,并且用户表,角色及其链接表以及外键将根据模型(实体包)自动生成,我们现在将继续创建。
4.添加实体,控制器,服务,存储库和视图
4.1。 添加实体(模型)
所有实体的强制性要求 :私有字段,所有字段的getter和setter以及空的构造函数(示例中未显示)。 他们不需要手动编写,按Alt + Insert ,Idea会为您完成。
要导入必要的类和库,请使用键盘快捷键Alt + Enter 。

用户名 @Entity @Table(name = "t_user") public class User implements UserDetails { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Size(min=2, message = " 5 ") private String username; @Size(min=2, message = " 5 ") private String password; @Transient private String passwordConfirm; @ManyToMany(fetch = FetchType.EAGER) private Set<Role> roles; public User() { } public Long getId() { return id; } public void setId(Long id) { this.id = id; } @Override public String getUsername() { return username; } @Override public boolean isAccountNonExpired() { return true; } @Override public boolean isAccountNonLocked() { return true; } @Override public boolean isCredentialsNonExpired() { return true; } @Override public boolean isEnabled() { return true; } public void setUsername(String username) { this.username = username; } @Override public Collection<? extends GrantedAuthority> getAuthorities() { return getRoles(); } @Override public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } public String getPasswordConfirm() { return passwordConfirm; } public void setPasswordConfirm(String passwordConfirm) { this.passwordConfirm = passwordConfirm; } public Set<Role> getRoles() { return roles; } public void setRoles(Set<Role> roles) { this.roles = roles; } }
用户名 在关于注释的开头:
Entity表示该类的字段在数据库中具有映射,
Table(名称=“ t_user”)指示哪个表。
GenerationType.IDENTITY IDENTITY参数意味着将使用数据库来生成ID。 还有其他策略。 SEQUENCE-使用内置数据库引擎(例如PostgreSQL或Oracle)来生成序列值(序列)的机制。 TABLE-使用带有初始化键值的单独表。 另一个选项是AUTO,休眠本身会选择上述策略之一,但是建议明确指定策略。
暂态注释下的字段在数据库中不显示。 角色列表与用户相关联,具有多对多关系(一个用户一方面可以具有多个角色,而另一个角色可以具有多个用户);
FetchType.EAGER- “贪婪”下载,即 角色列表将立即与用户一起加载(不要等到联系到他们之后)。
为了在Spring Security中进一步使用
User类,它必须实现
UserDetails接口。 为此,请覆盖其所有方法。 但是在我们的示例中,我们将仅使用
getAuthorities()方法,它返回用户角色的列表。 因此,对于其余方法,请将返回值更改为
true 。
角色角色 @Entity @Table(name = "t_role") public class Role implements GrantedAuthority { @Id private Long id; private String name; @Transient @ManyToMany(mappedBy = "roles") private Set<User> users; public Role() { } public Role(Long id) { this.id = id; } public Role(Long id, String name) { this.id = id; this.name = name; } public Long getId() { return id; } public void setId(Long id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } public Set<User> getUsers() { return users; } public void setUsers(Set<User> users) { this.users = users; } @Override public String getAuthority() { return getName(); } }
角色角色 此类必须实现
GrantedAuthority接口,在该接口中仅需要重新定义一个
getAuthority()方法(返回角色的名称)。 角色名称必须与模式“ ROLE_NAME”匹配,例如
ROLE_USER 。 除了默认的构造函数之外,还必须添加几个公共的构造函数:第一个仅接受id,第二个仅接受id和name。
您可以在此处添加字段限制。
大小(最小值= 2) -表示字段的最小长度为2,如果违反了限制,将显示一条消息。
4.2。 实施数据访问层和服务层
Spring Data提供了一组现成的实现,用于创建一个提供对数据库访问权限的层。
JpaRepository接口提供了一组用于处理数据库的标准方法(findBy,save,deleteById等)。
UserRepository。 我们在存储库包中创建用户界面,并继承
JpaRepository <User,Long> ,指定
User类,其ID类型为
Long 。
public interface UserRepository extends JpaRepository<User, Long> { User findByUsername(String username); }
T.O. 只需创建一个接口并继承
JpaRepository,就可以执行标准的数据库查询。 如果您需要特定的方法,只需根据Idea提示将其添加到界面中即可。 例如,我们需要一种通过名称在数据库中搜索用户的方法。 我们编写返回对象的类型,然后IDE提供可能的选项。 即 在这种情况下,方法名称确定请求主体。

如有必要,可以在该方法上使用
Query批注,并以HQL或SQL编写查询(您需要添加nativeQuery = true)。
@Query(value = "SELECT nextval(pg_get_serial_sequence('t_user', 'id'))", nativeQuery = true) Long getNextId();
RoleRepository。 我们以相同的方式创建,这里不需要我们自己的方法。
public interface RoleRepository extends JpaRepository<Role, Long> { }
UserService。 包含应用程序业务逻辑的方法。 此类实现
UserDetailsService接口(对于Spring Security是必需的),您需要在其中重写一个
loadUserByUsername()方法。
在此类中,您可以看到另一种执行SQL查询的方法-使用EntityManager。
用户服务 @Service public class UserService implements UserDetailsService { @PersistenceContext private EntityManager em; @Autowired UserRepository userRepository; @Autowired RoleRepository roleRepository; @Autowired BCryptPasswordEncoder bCryptPasswordEncoder; @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { User user = userRepository.findByUsername(username); if (user == null) { throw new UsernameNotFoundException("User not found"); } return user; } public User findUserById(Long userId) { Optional<User> userFromDb = userRepository.findById(userId); return userFromDb.orElse(new User()); } public List<User> allUsers() { return userRepository.findAll(); } public boolean saveUser(User user) { User userFromDB = userRepository.findByUsername(user.getUsername()); if (userFromDB != null) { return false; } user.setRoles(Collections.singleton(new Role(1L, "ROLE_USER"))); user.setPassword(bCryptPasswordEncoder.encode(user.getPassword())); userRepository.save(user); return true; } public boolean deleteUser(Long userId) { if (userRepository.findById(userId).isPresent()) { userRepository.deleteById(userId); return true; } return false; } public List<User> usergtList(Long idMin) { return em.createQuery("SELECT u FROM User u WHERE u.id > :paramId", User.class) .setParameter("paramId", idMin).getResultList(); } }
考虑
saveUser(用户用户)方法。
public boolean saveUser(User user) { User userFromDB = userRepository.findByUsername(user.getUsername()); if (userFromDB != null) { return false; } user.setRoles(Collections.singleton(new Role(1L, "ROLE_USER"))); user.setPassword(bCryptPasswordEncoder.encode(user.getPassword())); userRepository.save(user); return true; }
首先,按用户名在数据库中执行搜索,如果已经存在同名用户,则该方法完成工作。 如果未使用用户名,则会添加ROLE_USER角色。 为了不以原始格式存储密码,以前使用
bCryptPasswordEncoder对密码进行了哈希
处理 。 然后,新用户将保存在数据库中。
4.3。 添加控制器
对于未以任何方式由服务器处理的页面,而只是返回页面,可以在配置中配置映射。 默认情况下,
登录页面由Spring Security控制器处理,因此不需要单独的控制器。
@Configuration public class MvcConfig implements WebMvcConfigurer { @Override public void addViewControllers(ViewControllerRegistry registry) { registry.addViewController("/login").setViewName("login"); registry.addViewController("/news").setViewName("news"); } }
RegistrationController。 注册页面需要一个单独的控制器。 为了处理GET请求,对POST
使用注释
@GetMapping(“ / registration”) -
@PostMapping(“ / registration”) 。
RegistrationController @Controller public class RegistrationController { @Autowired private UserService userService; @GetMapping("/registration") public String registration(Model model) { model.addAttribute("userForm", new User()); return "registration"; } @PostMapping("/registration") public String addUser(@ModelAttribute("userForm") @Valid User userForm, BindingResult bindingResult, Model model) { if (bindingResult.hasErrors()) { return "registration"; } if (!userForm.getPassword().equals(userForm.getPasswordConfirm())){ model.addAttribute("passwordError", " "); return "registration"; } if (!userService.saveUser(userForm)){ model.addAttribute("usernameError", " "); return "registration"; } return "redirect:/"; } }
要在页面中添加或获取内容,我们转向模型。 在GET请求中,将
User类的新的空对象添加到页面中。 这样做是为了在POST请求(用户名,密码,passwordComfirm)期间一次不从注册表单中获取数据,而是立即获取已填充的userForm对象。
addUser()方法
期望在GET请求期间添加的用户对象(userForm)作为参数。 注释
有效检查是否满足在字段上设置的限制,在这种情况下,长度至少为2个字符。 如果未满足限制,则
bindingResult将包含错误。
如果密码和确认密码不匹配,请在页面上添加一条消息并返回。 最后,我们尝试保存将用户添加到数据库。
如果已经存在同名用户,则
saveUser()方法返回false;如果该用户保存在数据库中,则返回true。 如果保存尝试失败,我们将添加错误消息并返回页面。 如果成功保存用户,请转到主页。
管理员 仅管理员用户有权访问管理员页面。
userList()方法没有什么新内容;它接收所有用户的数据并将其添加到页面中。
管理员 @Controller public class AdminController { @Autowired private UserService userService; @GetMapping("/admin") public String userList(Model model) { model.addAttribute("allUsers", userService.allUsers()); return "admin"; } @PostMapping("/admin") public String deleteUser(@RequestParam(required = true, defaultValue = "" ) Long userId, @RequestParam(required = true, defaultValue = "" ) String action, Model model) { if (action.equals("delete")){ userService.deleteUser(userId); } return "redirect:/admin"; } @GetMapping("/admin/gt/{userId}") public String gtUser(@PathVariable("userId") Long userId, Model model) { model.addAttribute("allUsers", userService.usergtList(userId)); return "admin"; } }
deleteUser()方法使用
RequestParam批注,即 该视图将具有必须传递两个参数的表单-userId和action。 链接将采用以下格式
:http://本地主机:8080 / admin?UserId = 24&action = delete执行此请求时,将删除id = 24的用户。
将参数传递给URL的另一种方法是使用
PathVariable 。 使用此批注,我们获得了URL的各个部分,对于
getUser()方法,URL如下所示:
http://本地主机:8080 / admin / gt / 24 ,在转换之后,将显示ID> 24的所有用户的列表。
安全设定WebSecurityConfig。 包含2个
BCryptPasswordEncoder和
AuthenticationManager Bean ,它们是先前在userService类中遇到的。
另外,
configure()方法
配置对各种站点资源的访问。 作为
antMatchers()方法的参数,
我们传递了要为其设置限制的路径。 然后,我们向用户指示该页面可用的角色。
Web安全配置 @Configuration @EnableWebSecurity public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Autowired UserService userService; @Bean public BCryptPasswordEncoder bCryptPasswordEncoder() { return new BCryptPasswordEncoder(); } @Override protected void configure(HttpSecurity httpSecurity) throws Exception { httpSecurity .csrf() .disable() .authorizeRequests()
4.4。 添加视图
index.jsp主页,下面有2个选项-来宾和授权用户。


index.jsp <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <%@ taglib prefix="sec" uri="http://www.springframework.org/security/tags" %> <%@ page language="java" contentType="text/html; charset=utf-8" pageEncoding="utf-8"%> <!DOCTYPE HTML> <html> <head> <title></title> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/> <link rel="stylesheet" type="text/css" href="${contextPath}/resources/css/style.css"> </head> <body> <div> <h3>${pageContext.request.userPrincipal.name}</h3> <sec:authorize access="!isAuthenticated()"> <h4><a href="/login"></a></h4> <h4><a href="/registration"></a></h4> </sec:authorize> <sec:authorize access="isAuthenticated()"> <h4><a href="/logout"></a></h4> </sec:authorize> <h4><a href="/news"> ( )</a></h4> <h4><a href="/admin"> ( )</a></h4> </div> </body> </html>
要为授权用户隐藏页面上的部分内容(链接到注册和授权页面),可以使用Spring Security标签库中的
authorize标签。 access参数接受多个表达式,例如,您可以根据用户角色
hasRole('ADMIN')设置限制。
registration.jsp注册页面。

registration.jsp <%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %> <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <%@ page language="java" contentType="text/html; charset=utf-8" pageEncoding="utf-8"%> <!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title></title> </head> <body> <div> <form:form method="POST" modelAttribute="userForm"> <h2></h2> <div> <form:input type="text" path="username" placeholder="Username" autofocus="true"></form:input> <form:errors path="username"></form:errors> ${usernameError} </div> <div> <form:input type="password" path="password" placeholder="Password"></form:input> </div> <div> <form:input type="password" path="passwordConfirm" placeholder="Confirm your password"></form:input> <form:errors path="password"></form:errors> ${passwordError} </div> <button type="submit"></button> </form:form> <a href="/"></a> </div> </body> </html>
在此页面上,使用了标签库中的form标签,借助它,userForm模型属性被捆绑(我们在控制器中的GET请求期间将其添加到页面中)和表单:
<form:form method="POST" modelAttribute="userForm">
您还必须指定绑定userForm属性的路径:
<form:input type="text" path="username" placeholder="Username"></form:input>
login.jsp登录页面。

login.jsp <%@ taglib prefix="sec" uri="http://www.springframework.org/security/tags" %> <%@ page language="java" contentType="text/html; charset=utf-8" pageEncoding="utf-8"%> <!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>Log in with your account</title> </head> <body> <sec:authorize access="isAuthenticated()"> <% response.sendRedirect("/"); %> </sec:authorize> <div> <form method="POST" action="/login"> <h2> </h2> <div> <input name="username" type="text" placeholder="Username" autofocus="true"/> <input name="password" type="password" placeholder="Password"/> <button type="submit">Log In</button> <h4><a href="/registration"></a></h4> </div> </form> </div> </body> </html>
如前所述,该页面默认由Spring控制器处理。 重要的是指定操作:
action =“ / login”和输入的名称。
admin.jsp管理页面。

admin.jsp <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <%@ page language="java" contentType="text/html; charset=utf-8" pageEncoding="utf-8"%> <!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>Log in with your account</title> <link rel="stylesheet" type="text/css" href="${contextPath}/resources/css/style.css"> </head> <body> <div> <table> <thead> <th>ID</th> <th>UserName</th> <th>Password</th> <th>Roles</th> </thead> <c:forEach items="${allUsers}" var="user"> <tr> <td>${user.id}</td> <td>${user.username}</td> <td>${user.password}</td> <td> <c:forEach items="${user.roles}" var="role">${role.name}; </c:forEach> </td> <td> <form action="${pageContext.request.contextPath}/admin" method="post"> <input type="hidden" name="userId" value="${user.id}"/> <input type="hidden" name="action" value="delete"/> <button type="submit">Delete</button> </form> </td> </tr> </c:forEach> </table> <a href="/"></a> </div> </body> </html>
news.jsp新闻页面是静态的。 它仅用于演示用户权限,因此是您选择的内容。
news.jsp <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <%@ page language="java" contentType="text/html; charset=utf-8" pageEncoding="utf-8"%> <!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title></title> </head> <body> <div> <h2> <br> .</h2> <a href="/"></a> </div> </body> </html>
5.
main Application :
@SpringBootApplication public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); } }
, , , .
spring , .
, – 3 . t_role:
SQL INSERT INTO public.t_role(id, name) VALUES (1, 'ROLE_USER'), (2, 'ROLE_ADMIN');
. -, . , - , :
SQL INSERT INTO public.t_user_roles(user_id, roles_id) VALUES (1, 2);

( 403) – .
http://localhost:8080/admin .
http://localhost:8080/news . , .
结论
, . . , User, Writer, .
css js , . , , Bootstrap js.
.- Registration and Login with Spring Boot, Spring Security, Spring Data JPA, Hibernate, MySQL, JSP, Bootstrap and Docker Compose
- Spring
- Spring Security/ Spring Security
- Spring
- : Spring 3 MVC + Spring Security + Hibernate