Spring MVC4 + Spring Security4 + Hibernate實例
在這篇教程文章中,我們將使用Spring Security,Hibernate+MySQL數據庫來集成構建一個成熟的Spring MVC應用程序。處理多對多映射關係,同時利用BCrypt格式加密密碼存儲,和使用自定義PersistentTokenRepository實現Hibernate HibernateTokenRepositoryImpl並提供了 "記住我" 功能,從數據庫檢索和更新記錄或在事務中刪除它們;在這個實例中全部採用註釋配置。
這個項目可以作爲您自己的Spring MVC項目集成 Spring Security 模板,免受搭建開發環境皮肉之苦。
注意:
這篇文章展示了一個完整的應用的完整代碼。爲了縮小本教程文章的篇幅,這裏需要省略一些基本的知識點文字描述。如果您有興趣瞭解這些細節,這個,這個和這個文章可能會幫助你。
先來做個概括:
這個項目顯示了一個簡單的用戶管理應用程序。您可以創建一個新用戶,編輯或刪除現有用戶,並列出所有用戶信息列表。一個用戶可以與一個或多個用戶配置(UserProfile)相關聯,這表現出了多對多的關係。應用程序的URL是使用 Spring Security 作訪問保護的。這意味着,基於對登錄用戶的角色來獲得判定URL是否被授予或禁止訪問。在視圖層,用戶將根據分配給他/她的角色只能看到被允許頁面內容,這些是在視圖層中使用Spring Security標籤來實現的。
以下是一些需要使用到的技術:
- Spring 4.2.5.RELEASE
- Spring Security 4.0.4.RELEASE
- Hibernate Core 4.3.11.Final
- validation-api 1.1.0.Final
- hibernate-validator 5.1.3.Final
- MySQL Server 5.6
- Maven 3
- JDK 1.7
- Tomcat 8.0.21
- Eclipse MARS.1 Release 4.5.1
- logback 1.1.7
現在就讓我們開始一步步地學習和實現吧!
第1步:創建目錄結構
以下將是最終的項目結構:
現在,讓我們解釋在上面提到的結構內容每個細節。
第2步: 更新 pom.xml 以包括必需的依懶
<modelVersion>4.0.0</modelVersion>
<groupId>com.yiibai.springmvc</groupId>
<artifactId>SpringMVCHibernateManyToManyCRUDExample</artifactId>
<packaging>war</packaging>
<version>1.0.0</version>
<name>SpringMVCHibernateWithSpringSecurityExample</name>
<properties>
<springframework.version>4.2.5.RELEASE</springframework.version>
<springsecurity.version>4.0.4.RELEASE</springsecurity.version>
<hibernate.version>4.3.11.Final</hibernate.version>
<mysql.connector.version>5.1.31</mysql.connector.version>
</properties>
<dependencies>
<!-- Spring -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>${springframework.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
<version>${springframework.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>${springframework.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-tx</artifactId>
<version>${springframework.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-orm</artifactId>
<version>${springframework.version}</version>
</dependency>
<!-- Spring Security -->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-web</artifactId>
<version>${springsecurity.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-config</artifactId>
<version>${springsecurity.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-taglibs</artifactId>
<version>${springsecurity.version}</version>
</dependency>
<!-- Hibernate -->
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-core</artifactId>
<version>${hibernate.version}</version>
</dependency>
<!-- jsr303 validation -->
<dependency>
<groupId>javax.validation</groupId>
<artifactId>validation-api</artifactId>
<version>1.1.0.Final</version>
</dependency>
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-validator</artifactId>
<version>5.1.3.Final</version>
</dependency>
<!-- MySQL -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>${mysql.connector.version}</version>
</dependency>
<!-- SLF4J/Logback -->
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.1.7</version>
</dependency>
<!-- Servlet+JSP+JSTL -->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>3.1.0</version>
</dependency>
<dependency>
<groupId>javax.servlet.jsp</groupId>
<artifactId>javax.servlet.jsp-api</artifactId>
<version>2.3.1</version>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>jstl</artifactId>
<version>1.2</version>
</dependency>
</dependencies>
<build>
<pluginManagement>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.2</version>
<configuration>
<source>1.7</source>
<target>1.7</target>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-war-plugin</artifactId>
<version>2.4</version>
<configuration>
<warSourceDirectory>src/main/webapp</warSourceDirectory>
<warName>SpringMVCHibernateWithSpringSecurityExample</warName>
<failOnMissingWebXml>false</failOnMissingWebXml>
</configuration>
</plugin>
</plugins>
</pluginManagement>
<finalName>SpringMVCHibernateWithSpringSecurityExample</finalName>
</build>
第3步: 配置安全
這是最重要的步驟,在我們的應用程序中添加的Spring Security是創建Spring Security的Java配置。
該配置將創建被稱爲負責所有安全的springSecurityFilterChain Servlet過濾程序(保護應用程序的URL,驗證提交用戶名和密碼,重定向到日誌等等)提供在應用程序內。
package com.yiibai.springmvc.security;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationTrustResolver;
import org.springframework.security.authentication.AuthenticationTrustResolverImpl;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.rememberme.PersistentTokenBasedRememberMeServices;
import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository;
@Configuration
@EnableWebSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
@Autowired
@Qualifier("customUserDetailsService")
UserDetailsService userDetailsService;
@Autowired
PersistentTokenRepository tokenRepository;
@Autowired
public void configureGlobalSecurity(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService);
auth.authenticationProvider(authenticationProvider());
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().antMatchers("/", "/list")
.access("hasRole('USER') or hasRole('ADMIN') or hasRole('DBA')")
.antMatchers("/newuser/\*\*", "/delete-user-\*").access("hasRole('ADMIN')").antMatchers("/edit-user-\*")
.access("hasRole('ADMIN') or hasRole('DBA')").and().formLogin().loginPage("/login")
.loginProcessingUrl("/login").usernameParameter("ssoId").passwordParameter("password").and()
.rememberMe().rememberMeParameter("remember-me").tokenRepository(tokenRepository)
.tokenValiditySeconds(86400).and().csrf().and().exceptionHandling().accessDeniedPage("/Access\_Denied");
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public DaoAuthenticationProvider authenticationProvider() {
DaoAuthenticationProvider authenticationProvider = new DaoAuthenticationProvider();
authenticationProvider.setUserDetailsService(userDetailsService);
authenticationProvider.setPasswordEncoder(passwordEncoder());
return authenticationProvider;
}
@Bean
public PersistentTokenBasedRememberMeServices getPersistentTokenBasedRememberMeServices() {
PersistentTokenBasedRememberMeServices tokenBasedservice = new PersistentTokenBasedRememberMeServices(
"remember-me", userDetailsService, tokenRepository);
return tokenBasedservice;
}
@Bean
public AuthenticationTrustResolver getAuthenticationTrustResolver() {
return new AuthenticationTrustResolverImpl();
}
}
如上圖所示,接入到URL被控制,如下所示:
- ‘/’ & ‘/list’ : 供所有用戶訪問;
- ‘/newuser’ & ‘/delete-user-*’ : 只供管理員(Admin)用戶訪問;
- ‘/edit-user-*’ : 供 Admin & DBA 用戶訪問;
由於我們存儲憑據在數據庫中,所以要在UserDetailsService中配置DaoAuthenticationProvider來處理。此外,爲了在數據庫加密密碼,我們選擇BCryptPasswordEncoder。 此外,由於我們也將提供記住我的功能,跟蹤令牌數據在數據庫中,我們配置PersistentTokenRepository 實現。
Spring Security帶有兩個PersistentTokenRepository的實現: JdbcTokenRepositoryImpl 和 InMemoryTokenRepositoryImpl. 我們可以選擇JdbcTokenRepositoryImpl[此文章演示了rememberMe和JdbcTokenRepositoryImpl]。但在我們的應用程序使用Hibernate,爲什麼不使用Hibernate來代替JDBC創建一個自定義的實現?下面是相同功能的一個嘗試。
package com.yiibai.springmvc.dao;
import java.util.Date;
import org.hibernate.Criteria;
import org.hibernate.criterion.Restrictions;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.web.authentication.rememberme.PersistentRememberMeToken;
import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;
import com.yiibai.springmvc.dao.AbstractDao;
import com.yiibai.springmvc.model.PersistentLogin;
@Repository("tokenRepositoryDao")
@Transactional
public class HibernateTokenRepositoryImpl extends AbstractDao<String, PersistentLogin>
implements PersistentTokenRepository {
static final Logger logger = LoggerFactory.getLogger(HibernateTokenRepositoryImpl.class);
@Override
public void createNewToken(PersistentRememberMeToken token) {
logger.info("Creating Token for user : {}", token.getUsername());
PersistentLogin persistentLogin = new PersistentLogin();
persistentLogin.setUsername(token.getUsername());
persistentLogin.setSeries(token.getSeries());
persistentLogin.setToken(token.getTokenValue());
persistentLogin.setLast\_used(token.getDate());
persist(persistentLogin);
}
@Override
public PersistentRememberMeToken getTokenForSeries(String seriesId) {
logger.info("Fetch Token if any for seriesId : {}", seriesId);
try {
Criteria crit = createEntityCriteria();
crit.add(Restrictions.eq("series", seriesId));
PersistentLogin persistentLogin = (PersistentLogin) crit.uniqueResult();
return new PersistentRememberMeToken(persistentLogin.getUsername(), persistentLogin.getSeries(),
persistentLogin.getToken(), persistentLogin.getLast\_used());
} catch (Exception e) {
logger.info("Token not found...");
return null;
}
}
@Override
public void removeUserTokens(String username) {
logger.info("Removing Token if any for user : {}", username);
Criteria crit = createEntityCriteria();
crit.add(Restrictions.eq("username", username));
PersistentLogin persistentLogin = (PersistentLogin) crit.uniqueResult();
if (persistentLogin != null) {
logger.info("rememberMe was selected");
delete(persistentLogin);
}
}
@Override
public void updateToken(String seriesId, String tokenValue, Date lastUsed) {
logger.info("Updating Token for seriesId : {}", seriesId);
PersistentLogin persistentLogin = getByKey(seriesId);
persistentLogin.setToken(tokenValue);
persistentLogin.setLast\_used(lastUsed);
update(persistentLogin);
}
}
上述實現使用實體[PersistentLogin]映射到persistent_logins表,如下圖所示是實體本身。
package com.yiibai.springmvc.model;
import java.io.Serializable;
import java.util.Date;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Table;
import javax.persistence.Temporal;
import javax.persistence.TemporalType;
@Entity
@Table(name="PERSISTENT_LOGINS")
public class PersistentLogin implements Serializable{
@Id
private String series;
@Column(name="USERNAME", unique=true, nullable=false)
private String username;
@Column(name="TOKEN", unique=true, nullable=false)
private String token;
@Temporal(TemporalType.TIMESTAMP)
private Date last\_used;
public String getSeries() {
return series;
}
public void setSeries(String series) {
this.series = series;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getToken() {
return token;
}
public void setToken(String token) {
this.token = token;
}
public Date getLast\_used() {
return last\_used;
}
public void setLast\_used(Date last\_used) {
this.last\_used = last\_used;
}
}
這個 UserDetailsService 實現,在安全性配置中使用如下圖所示:
package com.yiibai.springmvc.security;
import java.util.ArrayList;
import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
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;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import com.yiibai.springmvc.model.User;
import com.yiibai.springmvc.model.UserProfile;
import com.yiibai.springmvc.service.UserService;
@Service("customUserDetailsService")
public class CustomUserDetailsService implements UserDetailsService{
static final Logger logger = LoggerFactory.getLogger(CustomUserDetailsService.class);
@Autowired
private UserService userService;
@Transactional(readOnly=true)
public UserDetails loadUserByUsername(String ssoId)
throws UsernameNotFoundException {
User user = userService.findBySSO(ssoId);
logger.info("User : {}", user);
if(user==null){
logger.info("User not found");
throw new UsernameNotFoundException("Username not found");
}
return new org.springframework.security.core.userdetails.User(user.getSsoId(), user.getPassword(),
true, true, true, true, getGrantedAuthorities(user));
}
private List<GrantedAuthority> getGrantedAuthorities(User user){
List<GrantedAuthority> authorities = new ArrayList<GrantedAuthority>();
for(UserProfile userProfile : user.getUserProfiles()){
logger.info("UserProfile : {}", userProfile);
authorities.add(new SimpleGrantedAuthority("ROLE\_"+userProfile.getType()));
}
logger.info("authorities : {}", authorities);
return authorities;
}
}
最後,使用下述初始化類註冊 springSecurityFilter 應用程序 war。
package com.yiibai.springmvc.security;
import org.springframework.security.web.context.AbstractSecurityWebApplicationInitializer;
public class SecurityWebApplicationInitializer extends AbstractSecurityWebApplicationInitializer {
}
這就是 Spring Security 配置。現在,讓我們從Spring MVC部分開始,討論Hibernate配置,必要的DAO,模型和服務。
第4步: 配置Hibernate
package com.yiibai.springmvc.configuration;
import java.util.Properties;
import javax.sql.DataSource;
import org.hibernate.SessionFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
import org.springframework.core.env.Environment;
import org.springframework.jdbc.datasource.DriverManagerDataSource;
import org.springframework.orm.hibernate4.HibernateTransactionManager;
import org.springframework.orm.hibernate4.LocalSessionFactoryBean;
import org.springframework.transaction.annotation.EnableTransactionManagement;
@Configuration
@EnableTransactionManagement
@ComponentScan({ "com.yiibai.springmvc.configuration" })
@PropertySource(value = { "classpath:application.properties" })
public class HibernateConfiguration {
@Autowired
private Environment environment;
@Bean
public LocalSessionFactoryBean sessionFactory() {
LocalSessionFactoryBean sessionFactory = new LocalSessionFactoryBean();
sessionFactory.setDataSource(dataSource());
sessionFactory.setPackagesToScan(new String\[\] { "com.yiibai.springmvc.model" });
sessionFactory.setHibernateProperties(hibernateProperties());
return sessionFactory;
}
@Bean
public DataSource dataSource() {
DriverManagerDataSource dataSource = new DriverManagerDataSource();
dataSource.setDriverClassName(environment.getRequiredProperty("jdbc.driverClassName"));
dataSource.setUrl(environment.getRequiredProperty("jdbc.url"));
dataSource.setUsername(environment.getRequiredProperty("jdbc.username"));
dataSource.setPassword(environment.getRequiredProperty("jdbc.password"));
return dataSource;
}
private Properties hibernateProperties() {
Properties properties = new Properties();
properties.put("hibernate.dialect", environment.getRequiredProperty("hibernate.dialect"));
properties.put("hibernate.show\_sql", environment.getRequiredProperty("hibernate.show\_sql"));
properties.put("hibernate.format\_sql", environment.getRequiredProperty("hibernate.format\_sql"));
return properties;
}
@Bean
@Autowired
public HibernateTransactionManager transactionManager(SessionFactory s) {
HibernateTransactionManager txManager = new HibernateTransactionManager();
txManager.setSessionFactory(s);
return txManager;
}
}
下面是這篇教程文章中使用的屬性文件。
/src/main/resources/application.properties
jdbc.driverClassName = com.mysql.jdbc.Driver
jdbc.url = jdbc:mysql://localhost:3306/yiibai
jdbc.username = root
jdbc.password =
hibernate.dialect = org.hibernate.dialect.MySQLDialect
hibernate.show_sql = true
hibernate.format_sql = true
第5步:配置Spring MVC
package com.yiibai.springmvc.configuration;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.MessageSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.support.ResourceBundleMessageSource;
import org.springframework.format.FormatterRegistry;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.PathMatchConfigurer;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.ViewResolverRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
import org.springframework.web.servlet.view.InternalResourceViewResolver;
import org.springframework.web.servlet.view.JstlView;
import com.yiibai.springmvc.converter.RoleToUserProfileConverter;
@Configuration
@EnableWebMvc
@ComponentScan(basePackages = "com.yiibai.springmvc")
public class AppConfig extends WebMvcConfigurerAdapter{
@Autowired
RoleToUserProfileConverter roleToUserProfileConverter;
/\*\*
\* Configure ViewResolvers to deliver preferred views.
\*/
@Override
public void configureViewResolvers(ViewResolverRegistry registry) {
InternalResourceViewResolver viewResolver = new InternalResourceViewResolver();
viewResolver.setViewClass(JstlView.class);
viewResolver.setPrefix("/WEB-INF/views/");
viewResolver.setSuffix(".jsp");
registry.viewResolver(viewResolver);
}
/\*\*
\* Configure ResourceHandlers to serve static resources like CSS/ Javascript etc...
\*/
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/static/\*\*").addResourceLocations("/static/");
}
/\*\*
\* Configure Converter to be used.
\* In our example, we need a converter to convert string values\[Roles\] to UserProfiles in newUser.jsp
\*/
@Override
public void addFormatters(FormatterRegistry registry) {
registry.addConverter(roleToUserProfileConverter);
}
/\*\*
\* Configure MessageSource to lookup any validation/error message in internationalized property files
\*/
@Bean
public MessageSource messageSource() {
ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource();
messageSource.setBasename("messages");
return messageSource;
}
/\*\*Optional. It's only required when handling '.' in @PathVariables which otherwise ignore everything after last '.' in @PathVaidables argument.
\* It's a known bug in Spring \[https://jira.spring.io/browse/SPR-6164\], still present in Spring 4.1.7.
\* This is a workaround for this issue.
\*/
@Override
public void configurePathMatch(PathMatchConfigurer matcher) {
matcher.setUseRegisteredSuffixPatternMatch(true);
}
}
這種配置的主要亮點是RoleToUserProfileConverter。這將需要在數據庫視圖中的單個USERPROFILE ID映射到實際的 UserProfile實體。
package com.yiibai.springmvc.converter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.convert.converter.Converter;
import org.springframework.stereotype.Component;
import com.yiibai.springmvc.model.UserProfile;
import com.yiibai.springmvc.service.UserProfileService;
/**
* A converter class used in views to map id's to actual userProfile objects.
*/
@Component
public class RoleToUserProfileConverter implements Converter<Object, UserProfile>{
static final Logger logger = LoggerFactory.getLogger(RoleToUserProfileConverter.class);
@Autowired
UserProfileService userProfileService;
/\*\*
\* Gets UserProfile by Id
\* @see org.springframework.core.convert.converter.Converter#convert(java.lang.Object)
\*/
public UserProfile convert(Object element) {
Integer id = Integer.parseInt((String)element);
UserProfile profile= userProfileService.findById(id);
logger.info("Profile : {}",profile);
return profile;
}
}
由於我們使用JSR驗證應用程序驗證用戶輸入,我們已經配置在用戶在驗證失敗情況下顯示的消息。下圖所示爲 message.properties 文件內容:
NotEmpty.user.firstName=First name can not be blank.
NotEmpty.user.lastName=Last name can not be blank.
NotEmpty.user.email=Email can not be blank.
NotEmpty.user.password=Password can not be blank.
NotEmpty.user.ssoId=SSO ID can not be blank.
NotEmpty.user.userProfiles=At least one profile must be selected.
non.unique.ssoId=SSO ID {0} already exist. Please fill in different value.
最後,Spring初始化器類如下所示:
package com.yiibai.springmvc.configuration;
import org.springframework.web.servlet.support.AbstractAnnotationConfigDispatcherServletInitializer;
public class AppInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {
@Override
protected Class<?>\[\] getRootConfigClasses() {
return new Class\[\] { AppConfig.class };
}
@Override
protected Class<?>\[\] getServletConfigClasses() {
return null;
}
@Override
protected String\[\] getServletMappings() {
return new String\[\] { "/" };
}
}
第6步: 創建Spring控制器
package com.yiibai.springmvc.controller;
import java.util.List;
import java.util.Locale;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.validation.Valid;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.MessageSource;
import org.springframework.security.authentication.AuthenticationTrustResolver;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.web.authentication.rememberme.PersistentTokenBasedRememberMeServices;
import org.springframework.stereotype.Controller;
import org.springframework.ui.ModelMap;
import org.springframework.validation.BindingResult;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.SessionAttributes;
import com.yiibai.springmvc.model.User;
import com.yiibai.springmvc.model.UserProfile;
import com.yiibai.springmvc.service.UserProfileService;
import com.yiibai.springmvc.service.UserService;
@Controller
@RequestMapping("/")
@SessionAttributes("roles")
public class AppController {
@Autowired
UserService userService;
@Autowired
UserProfileService userProfileService;
@Autowired
MessageSource messageSource;
@Autowired
PersistentTokenBasedRememberMeServices persistentTokenBasedRememberMeServices;
@Autowired
AuthenticationTrustResolver authenticationTrustResolver;
/\*\*
\* This method will list all existing users.
\*/
@RequestMapping(value = { "/", "/list" }, method = RequestMethod.GET)
public String listUsers(ModelMap model) {
List<User> users = userService.findAllUsers();
model.addAttribute("users", users);
model.addAttribute("loggedinuser", getPrincipal());
return "userslist";
}
/\*\*
\* This method will provide the medium to add a new user.
\*/
@RequestMapping(value = { "/newuser" }, method = RequestMethod.GET)
public String newUser(ModelMap model) {
User user = new User();
model.addAttribute("user", user);
model.addAttribute("edit", false);
model.addAttribute("loggedinuser", getPrincipal());
return "registration";
}
/\*\*
\* This method will be called on form submission, handling POST request for
\* saving user in database. It also validates the user input
\*/
@RequestMapping(value = { "/newuser" }, method = RequestMethod.POST)
public String saveUser(@Valid User user, BindingResult result,
ModelMap model) {
if (result.hasErrors()) {
return "registration";
}
/\*
\* Preferred way to achieve uniqueness of field \[sso\] should be implementing custom @Unique annotation
\* and applying it on field \[sso\] of Model class \[User\].
\*
\* Below mentioned peace of code \[if block\] is to demonstrate that you can fill custom errors outside the validation
\* framework as well while still using internationalized messages.
\*
\*/
if(!userService.isUserSSOUnique(user.getId(), user.getSsoId())){
FieldError ssoError =new FieldError("user","ssoId",messageSource.getMessage("non.unique.ssoId", new String\[\]{user.getSsoId()}, Locale.getDefault()));
result.addError(ssoError);
return "registration";
}
userService.saveUser(user);
model.addAttribute("success", "User " + user.getFirstName() + " "+ user.getLastName() + " registered successfully");
model.addAttribute("loggedinuser", getPrincipal());
//return "success";
return "registrationsuccess";
}
/\*\*
\* This method will provide the medium to update an existing user.
\*/
@RequestMapping(value = { "/edit-user-{ssoId}" }, method = RequestMethod.GET)
public String editUser(@PathVariable String ssoId, ModelMap model) {
User user = userService.findBySSO(ssoId);
model.addAttribute("user", user);
model.addAttribute("edit", true);
model.addAttribute("loggedinuser", getPrincipal());
return "registration";
}
/\*\*
\* This method will be called on form submission, handling POST request for
\* updating user in database. It also validates the user input
\*/
@RequestMapping(value = { "/edit-user-{ssoId}" }, method = RequestMethod.POST)
public String updateUser(@Valid User user, BindingResult result,
ModelMap model, @PathVariable String ssoId) {
if (result.hasErrors()) {
return "registration";
}
/\*//Uncomment below 'if block' if you WANT TO ALLOW UPDATING SSO\_ID in UI which is a unique key to a User.
if(!userService.isUserSSOUnique(user.getId(), user.getSsoId())){
FieldError ssoError =new FieldError("user","ssoId",messageSource.getMessage("non.unique.ssoId", new String\[\]{user.getSsoId()}, Locale.getDefault()));
result.addError(ssoError);
return "registration";
}\*/
userService.updateUser(user);
model.addAttribute("success", "User " + user.getFirstName() + " "+ user.getLastName() + " updated successfully");
model.addAttribute("loggedinuser", getPrincipal());
return "registrationsuccess";
}
/\*\*
\* This method will delete an user by it's SSOID value.
\*/
@RequestMapping(value = { "/delete-user-{ssoId}" }, method = RequestMethod.GET)
public String deleteUser(@PathVariable String ssoId) {
userService.deleteUserBySSO(ssoId);
return "redirect:/list";
}
/\*\*
\* This method will provide UserProfile list to views
\*/
@ModelAttribute("roles")
public List<UserProfile> initializeProfiles() {
return userProfileService.findAll();
}
/\*\*
\* This method handles Access-Denied redirect.
\*/
@RequestMapping(value = "/Access\_Denied", method = RequestMethod.GET)
public String accessDeniedPage(ModelMap model) {
model.addAttribute("loggedinuser", getPrincipal());
return "accessDenied";
}
/\*\*
\* This method handles login GET requests.
\* If users is already logged-in and tries to goto login page again, will be redirected to list page.
\*/
@RequestMapping(value = "/login", method = RequestMethod.GET)
public String loginPage() {
if (isCurrentAuthenticationAnonymous()) {
return "login";
} else {
return "redirect:/list";
}
}
/\*\*
\* This method handles logout requests.
\* Toggle the handlers if you are RememberMe functionality is useless in your app.
\*/
@RequestMapping(value="/logout", method = RequestMethod.GET)
public String logoutPage (HttpServletRequest request, HttpServletResponse response){
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
if (auth != null){
//new SecurityContextLogoutHandler().logout(request, response, auth);
persistentTokenBasedRememberMeServices.logout(request, response, auth);
SecurityContextHolder.getContext().setAuthentication(null);
}
return "redirect:/login?logout";
}
/\*\*
\* This method returns the principal\[user-name\] of logged-in user.
\*/
private String getPrincipal(){
String userName = null;
Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
if (principal instanceof UserDetails) {
userName = ((UserDetails)principal).getUsername();
} else {
userName = principal.toString();
}
return userName;
}
/\*\*
\* This method returns true if users is already authenticated \[logged-in\], else false.
\*/
private boolean isCurrentAuthenticationAnonymous() {
final Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
return authenticationTrustResolver.isAnonymous(authentication);
}
}
這是一個很小的Spring MVC控制器。對每種方法的視圖我們也提供了一些解釋。
第7步: 創建模型
package com.yiibai.springmvc.model;
import java.io.Serializable;
import java.util.HashSet;
import java.util.Set;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.JoinTable;
import javax.persistence.ManyToMany;
import javax.persistence.Table;
import org.hibernate.validator.constraints.NotEmpty;
@Entity
@Table(name="APP_USER")
public class User implements Serializable{
@Id @GeneratedValue(strategy=GenerationType.IDENTITY)
private Integer id;
@NotEmpty
@Column(name="SSO\_ID", unique=true, nullable=false)
private String ssoId;
@NotEmpty
@Column(name="PASSWORD", nullable=false)
private String password;
@NotEmpty
@Column(name="FIRST\_NAME", nullable=false)
private String firstName;
@NotEmpty
@Column(name="LAST\_NAME", nullable=false)
private String lastName;
@NotEmpty
@Column(name="EMAIL", nullable=false)
private String email;
@NotEmpty
@ManyToMany(fetch = FetchType.LAZY)
@JoinTable(name = "APP\_USER\_USER\_PROFILE",
joinColumns = { @JoinColumn(name = "USER\_ID") },
inverseJoinColumns = { @JoinColumn(name = "USER\_PROFILE\_ID") })
private Set<UserProfile> userProfiles = new HashSet<UserProfile>();
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getSsoId() {
return ssoId;
}
public void setSsoId(String ssoId) {
this.ssoId = ssoId;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public String getFirstName() {
return firstName;
}
public void setFirstName(String firstName) {
this.firstName = firstName;
}
public String getLastName() {
return lastName;
}
public void setLastName(String lastName) {
this.lastName = lastName;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public Set<UserProfile> getUserProfiles() {
return userProfiles;
}
public void setUserProfiles(Set<UserProfile> userProfiles) {
this.userProfiles = userProfiles;
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime \* result + ((id == null) ? 0 : id.hashCode());
result = prime \* result + ((ssoId == null) ? 0 : ssoId.hashCode());
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (!(obj instanceof User))
return false;
User other = (User) obj;
if (id == null) {
if (other.id != null)
return false;
} else if (!id.equals(other.id))
return false;
if (ssoId == null) {
if (other.ssoId != null)
return false;
} else if (!ssoId.equals(other.ssoId))
return false;
return true;
}
/\*
\* DO-NOT-INCLUDE passwords in toString function.
\* It is done here just for convenience purpose.
\*/
@Override
public String toString() {
return "User \[id=" + id + ", ssoId=" + ssoId + ", password=" + password
+ ", firstName=" + firstName + ", lastName=" + lastName
+ ", email=" + email + "\]";
}
}
package com.yiibai.springmvc.model;
import java.io.Serializable;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Table;
@Entity
@Table(name="USER_PROFILE")
public class UserProfile implements Serializable{
@Id @GeneratedValue(strategy=GenerationType.IDENTITY)
private Integer id;
@Column(name="TYPE", length=15, unique=true, nullable=false)
private String type = UserProfileType.USER.getUserProfileType();
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getType() {
return type;
}
public void setType(String type) {
this.type = type;
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime \* result + ((id == null) ? 0 : id.hashCode());
result = prime \* result + ((type == null) ? 0 : type.hashCode());
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (!(obj instanceof UserProfile))
return false;
UserProfile other = (UserProfile) obj;
if (id == null) {
if (other.id != null)
return false;
} else if (!id.equals(other.id))
return false;
if (type == null) {
if (other.type != null)
return false;
} else if (!type.equals(other.type))
return false;
return true;
}
@Override
public String toString() {
return "UserProfile \[id=" + id + ", type=" + type + "\]";
}
}
package com.yiibai.springmvc.model;
import java.io.Serializable;
public enum UserProfileType implements Serializable{
USER("USER"),
DBA("DBA"),
ADMIN("ADMIN");
String userProfileType;
private UserProfileType(String userProfileType){
this.userProfileType = userProfileType;
}
public String getUserProfileType(){
return userProfileType;
}
}
第7步: 創建DAO
package com.yiibai.springmvc.dao;
import java.io.Serializable;
import java.lang.reflect.ParameterizedType;
import org.hibernate.Criteria;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.springframework.beans.factory.annotation.Autowired;
public abstract class AbstractDao<PK extends Serializable, T> {
private final Class<T> persistentClass;
@SuppressWarnings("unchecked")
public AbstractDao(){
this.persistentClass =(Class<T>) ((ParameterizedType) this.getClass().getGenericSuperclass()).getActualTypeArguments()\[1\];
}
@Autowired
private SessionFactory sessionFactory;
protected Session getSession(){
return sessionFactory.getCurrentSession();
}
@SuppressWarnings("unchecked")
public T getByKey(PK key) {
return (T) getSession().get(persistentClass, key);
}
public void persist(T entity) {
getSession().persist(entity);
}
public void update(T entity) {
getSession().update(entity);
}
public void delete(T entity) {
getSession().delete(entity);
}
protected Criteria createEntityCriteria(){
return getSession().createCriteria(persistentClass);
}
}
package com.yiibai.springmvc.dao;
import java.util.List;
import com.yiibai.springmvc.model.User;
public interface UserDao {
User findById(int id);
User findBySSO(String sso);
void save(User user);
void deleteBySSO(String sso);
List<User> findAllUsers();
}
package com.yiibai.springmvc.dao;
import java.util.List;
import org.hibernate.Criteria;
import org.hibernate.Hibernate;
import org.hibernate.criterion.Order;
import org.hibernate.criterion.Restrictions;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Repository;
import com.yiibai.springmvc.model.User;
@Repository("userDao")
public class UserDaoImpl extends AbstractDao<Integer, User> implements UserDao {
static final Logger logger = LoggerFactory.getLogger(UserDaoImpl.class);
public User findById(int id) {
User user = getByKey(id);
if(user!=null){
Hibernate.initialize(user.getUserProfiles());
}
return user;
}
public User findBySSO(String sso) {
logger.info("SSO : {}", sso);
Criteria crit = createEntityCriteria();
crit.add(Restrictions.eq("ssoId", sso));
User user = (User)crit.uniqueResult();
if(user!=null){
Hibernate.initialize(user.getUserProfiles());
}
return user;
}
@SuppressWarnings("unchecked")
public List<User> findAllUsers() {
Criteria criteria = createEntityCriteria().addOrder(Order.asc("firstName"));
criteria.setResultTransformer(Criteria.DISTINCT\_ROOT\_ENTITY);//To avoid duplicates.
List<User> users = (List<User>) criteria.list();
// No need to fetch userProfiles since we are not showing them on list page. Let them lazy load.
// Uncomment below lines for eagerly fetching of userProfiles if you want.
/\*
for(User user : users){
Hibernate.initialize(user.getUserProfiles());
}\*/
return users;
}
public void save(User user) {
persist(user);
}
public void deleteBySSO(String sso) {
Criteria crit = createEntityCriteria();
crit.add(Restrictions.eq("ssoId", sso));
User user = (User)crit.uniqueResult();
delete(user);
}
}
package com.yiibai.springmvc.dao;
import java.util.List;
import com.yiibai.springmvc.model.UserProfile;
public interface UserProfileDao {
List<UserProfile> findAll();
UserProfile findByType(String type);
UserProfile findById(int id);
}
package com.yiibai.springmvc.dao;
import java.util.List;
import org.hibernate.Criteria;
import org.hibernate.criterion.Order;
import org.hibernate.criterion.Restrictions;
import org.springframework.stereotype.Repository;
import com.yiibai.springmvc.model.UserProfile;
@Repository("userProfileDao")
public class UserProfileDaoImpl extends AbstractDao<Integer, UserProfile>implements UserProfileDao{
public UserProfile findById(int id) {
return getByKey(id);
}
public UserProfile findByType(String type) {
Criteria crit = createEntityCriteria();
crit.add(Restrictions.eq("type", type));
return (UserProfile) crit.uniqueResult();
}
@SuppressWarnings("unchecked")
public List<UserProfile> findAll(){
Criteria crit = createEntityCriteria();
crit.addOrder(Order.asc("type"));
return (List<UserProfile>)crit.list();
}
}
第8步: 創建Services
package com.yiibai.springmvc.service;
import java.util.List;
import com.yiibai.springmvc.model.User;
public interface UserService {
User findById(int id);
User findBySSO(String sso);
void saveUser(User user);
void updateUser(User user);
void deleteUserBySSO(String sso);
List<User> findAllUsers();
boolean isUserSSOUnique(Integer id, String sso);
}
package com.yiibai.springmvc.service;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import com.yiibai.springmvc.dao.UserDao;
import com.yiibai.springmvc.model.User;
@Service("userService")
@Transactional
public class UserServiceImpl implements UserService{
@Autowired
private UserDao dao;
@Autowired
private PasswordEncoder passwordEncoder;
public User findById(int id) {
return dao.findById(id);
}
public User findBySSO(String sso) {
User user = dao.findBySSO(sso);
return user;
}
public void saveUser(User user) {
user.setPassword(passwordEncoder.encode(user.getPassword()));
dao.save(user);
}
/\*
\* Since the method is running with Transaction, No need to call hibernate update explicitly.
\* Just fetch the entity from db and update it with proper values within transaction.
\* It will be updated in db once transaction ends.
\*/
public void updateUser(User user) {
User entity = dao.findById(user.getId());
if(entity!=null){
entity.setSsoId(user.getSsoId());
if(!user.getPassword().equals(entity.getPassword())){
entity.setPassword(passwordEncoder.encode(user.getPassword()));
}
entity.setFirstName(user.getFirstName());
entity.setLastName(user.getLastName());
entity.setEmail(user.getEmail());
entity.setUserProfiles(user.getUserProfiles());
}
}
public void deleteUserBySSO(String sso) {
dao.deleteBySSO(sso);
}
public List<User> findAllUsers() {
return dao.findAllUsers();
}
public boolean isUserSSOUnique(Integer id, String sso) {
User user = findBySSO(sso);
return ( user == null || ((id != null) && (user.getId() == id)));
}
}
package com.yiibai.springmvc.service;
import java.util.List;
import com.yiibai.springmvc.model.UserProfile;
public interface UserProfileService {
UserProfile findById(int id);
UserProfile findByType(String type);
List<UserProfile> findAll();
}
package com.yiibai.springmvc.service;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import com.yiibai.springmvc.dao.UserProfileDao;
import com.yiibai.springmvc.model.UserProfile;
@Service("userProfileService")
@Transactional
public class UserProfileServiceImpl implements UserProfileService{
@Autowired
UserProfileDao dao;
public UserProfile findById(int id) {
return dao.findById(id);
}
public UserProfile findByType(String type){
return dao.findByType(type);
}
public List<UserProfile> findAll() {
return dao.findAll();
}
}
第9步: 創建視圖
從登錄頁面開始,要求輸入用戶名和密碼,以及可選「記住我」的標誌。
WEB-INF/views/login.jsp
<%@ page language="java" contentType="text/html; charset=ISO-8859-1" pageEncoding="ISO-8859-1"%>
<%@ page isELIgnored="false" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<body>
<div id="mainWrapper">
<div class="login-container">
<div class="login-card">
<div class="login-form">
<c:url var="loginUrl" value="/login" />
<form action="${loginUrl}" method="post" class="form-horizontal">
<c:if test="${param.error != null}">
<div class="alert alert-danger">
<p>Invalid username and password.</p>
</div>
</c:if>
<c:if test="${param.logout != null}">
<div class="alert alert-success">
<p>You have been logged out successfully.</p>
</div>
</c:if>
<div class="input-group input-sm">
<label class="input-group-addon" for="username"><i class="fa fa-user"></i></label>
<input type="text" class="form-control" id="username" name="ssoId" placeholder="Enter Username" required>
</div>
<div class="input-group input-sm">
<label class="input-group-addon" for="password"><i class="fa fa-lock"></i></label>
<input type="password" class="form-control" id="password" name="password" placeholder="Enter Password" required>
</div>
<div class="input-group input-sm">
<div class="checkbox">
<label><input type="checkbox" id="rememberme" name="remember-me"> Remember Me</label>
</div>
</div>
<input type="hidden" name="${\_csrf.parameterName}" value="${\_csrf.token}" />
<div class="form-actions">
<input type="submit"
class="btn btn-block btn-primary btn-default" value="Log in">
</div>
</form>
</div>
</div>
</div>
</div>
</body>
當用戶登錄成功後,將呈現列表頁面並顯示現有的所有用戶。要特別注意下面 Spring Security 標籤的使用。添加,編輯和刪除的鏈接/按鈕的顯示是基於角色,所以‘User’角色的用戶不能看到它們。你可能會問:如果直接在瀏覽器欄輸入網址呢?我們已經在 Spring Security 中配置了URL,因此無後顧之憂。
WEB-INF/views/userslist.jsp
<%@ page language="java" contentType="text/html; charset=ISO-8859-1" pageEncoding="ISO-8859-1"%>
<%@ page isELIgnored="false" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ taglib prefix="sec" uri="http://www.springframework.org/security/tags"%>
Firstname | Lastname | SSO ID |
---|