spring security使用hibernate进行查询数据库验证
前面查询数据库采用的都是jdbc方式,如果系统使用的是hibernate,该如何进行呢,下面就是实现步骤,关键还是实现自定义的UserDetailsService
项目结构如下:
使用hibernate,pom.xml文件如下:
<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/maven-v4_0_0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.petter</groupId> <artifactId>security-hibernate-annotation</artifactId> <packaging>war</packaging> <version>1.0-SNAPSHOT</version> <name>security-hibernate-annotation Maven Webapp</name> <url>http://maven.apache.org</url> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> <java.version>1.8</java.version> <spring.version>4.3.5.RELEASE</spring.version> <spring.security.version>4.2.1.RELEASE</spring.security.version> <mysql.connector.version>5.1.40</mysql.connector.version> <dbcp.version>2.1.1</dbcp.version> <hibernate.version>5.2.9.Final</hibernate.version> </properties> <dependencies> <!-- database pool --> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-dbcp2</artifactId> <version>${dbcp.version}</version> </dependency> <!-- Hibernate ORM --> <dependency> <groupId>org.hibernate</groupId> <artifactId>hibernate-core</artifactId> <version>${hibernate.version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-core</artifactId> <version>${spring.version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-web</artifactId> <version>${spring.version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-webmvc</artifactId> <version>${spring.version}</version> </dependency> <!-- ORM integration, e.g Hibernate --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-orm</artifactId> <version>${spring.version}</version> </dependency> <!-- Spring + aspects --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-aspects</artifactId> <version>${spring.version}</version> </dependency> <dependency> <groupId>org.thymeleaf</groupId> <artifactId>thymeleaf-spring4</artifactId> <version>3.0.3.RELEASE</version> </dependency> <!-- Spring Security --> <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-web</artifactId> <version>${spring.security.version}</version> </dependency> <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-config</artifactId> <version>${spring.security.version}</version> </dependency> <!-- 用于thymeleaf中使用security的标签 --> <dependency> <groupId>org.thymeleaf.extras</groupId> <artifactId>thymeleaf-extras-springsecurity4</artifactId> <version>3.0.2.RELEASE</version> </dependency> <!-- mysql --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>${mysql.connector.version}</version> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <version>3.3</version> <configuration> <source>1.8</source> <target>1.8</target> <encoding>utf8</encoding> </configuration> </plugin> </plugins> </build> </project>
一、由于使用hibernate,我们使用jpa自己生成数据库表,具体是
1、用于实现基于持久化Token的记住我功能的表persistent_logins,对应的类是PersistentLogin
package com.petter.model; import javax.persistence.*; import java.util.Date; /** * @author hongxf * @since 2017-04-17 14:42 */ @Entity @Table(name = "persistent_logins") public class PersistentLogin { private String series; private String username; private String token; private Date lastUsed; @Id @Column(name = "series") public String getSeries() { return series; } public void setSeries(String series) { this.series = series; } @Column(name = "username", nullable = false) public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } @Column(name = "token", nullable = false) public String getToken() { return token; } public void setToken(String token) { this.token = token; } @Temporal(TemporalType.TIMESTAMP) @Column(name = "last_used", nullable = false) public Date getLastUsed() { return lastUsed; } public void setLastUsed(Date lastUsed) { this.lastUsed = lastUsed; } }
2、用户类User对应表users
package com.petter.model; import javax.persistence.*; import java.util.HashSet; import java.util.Set; /** * @author hongxf * @since 2017-04-17 10:29 */ @Entity @Table(name = "users") public class User { private String username; private String password; private boolean enabled; private boolean accountNonExpired; private boolean accountNonLocked; private boolean credentialsNonExpired; private Set<UserRole> userRole = new HashSet<>(); @Id @Column(name = "username", unique = true, nullable = false, length = 45) public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } @Column(name = "password", nullable = false, length = 60) public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } @Column(name = "enable", nullable = false) public boolean isEnabled() { return enabled; } public void setEnabled(boolean enabled) { this.enabled = enabled; } @Column(name = "accountNonExpired", nullable = false) public boolean isAccountNonExpired() { return accountNonExpired; } public void setAccountNonExpired(boolean accountNonExpired) { this.accountNonExpired = accountNonExpired; } @Column(name = "accountNonLocked", nullable = false) public boolean isAccountNonLocked() { return accountNonLocked; } public void setAccountNonLocked(boolean accountNonLocked) { this.accountNonLocked = accountNonLocked; } @Column(name = "credentialsNonExpired", nullable = false) public boolean isCredentialsNonExpired() { return credentialsNonExpired; } public void setCredentialsNonExpired(boolean credentialsNonExpired) { this.credentialsNonExpired = credentialsNonExpired; } @OneToMany(fetch = FetchType.EAGER, mappedBy = "user") public Set<UserRole> getUserRole() { return userRole; } public void setUserRole(Set<UserRole> userRole) { this.userRole = userRole; } }
3、用户权限表user_roles对应类UserRole
package com.petter.model; import javax.persistence.*; /** * @author hongxf * @since 2017-04-17 10:32 */ @Entity @Table(name = "user_roles", uniqueConstraints = @UniqueConstraint(columnNames = {"role", "username"})) public class UserRole { private Integer userRoleId; private User user; private String role; @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name = "user_role_id") public Integer getUserRoleId() { return userRoleId; } public void setUserRoleId(Integer userRoleId) { this.userRoleId = userRoleId; } @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "username", nullable = false) public User getUser() { return user; } public void setUser(User user) { this.user = user; } @Column(name = "role", nullable = false, length = 45) public String getRole() { return role; } public void setRole(String role) { this.role = role; } }
4、用于保存登录失败尝试次数的UserAttempts
package com.petter.model; import javax.persistence.*; import java.util.Date; /** * @author hongxf * @since 2017-03-20 10:50 */ @Entity @Table(name = "user_attempts") public class UserAttempts { private int id; private String username; private int attempts; private Date lastModified; @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name = "id") public int getId() { return id; } public void setId(int id) { this.id = id; } @Column(name = "username") public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } @Column(name = "attempts") public int getAttempts() { return attempts; } public void setAttempts(int attempts) { this.attempts = attempts; } @Temporal(TemporalType.TIMESTAMP) @Column(name = "lastModified") public Date getLastModified() { return lastModified; } public void setLastModified(Date lastModified) { this.lastModified = lastModified; } }
二、配置hibernate,在AppConfig类中添加hibernate的配置
package com.petter.config; import org.apache.commons.dbcp2.BasicDataSource; import org.hibernate.SessionFactory; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; import org.springframework.orm.hibernate5.HibernateTransactionManager; import org.springframework.orm.hibernate5.LocalSessionFactoryBuilder; import org.springframework.transaction.annotation.EnableTransactionManagement; import org.springframework.web.servlet.config.annotation.EnableWebMvc; import org.thymeleaf.extras.springsecurity4.dialect.SpringSecurityDialect; import org.thymeleaf.spring4.SpringTemplateEngine; import org.thymeleaf.spring4.templateresolver.SpringResourceTemplateResolver; import org.thymeleaf.spring4.view.ThymeleafViewResolver; import org.thymeleaf.templatemode.TemplateMode; import java.util.Properties; /** * 相当于 * @author hongxf * @since 2017-03-08 10:11 */ @EnableWebMvc @Configuration @ComponentScan({"com.petter.*"}) @EnableTransactionManagement @Import({SecurityConfig.class}) public class AppConfig { @Bean public SessionFactory sessionFactory() { LocalSessionFactoryBuilder builder = new LocalSessionFactoryBuilder(dataSource()); builder.scanPackages("com.petter.model") .addProperties(getHibernateProperties()); return builder.buildSessionFactory(); } private Properties getHibernateProperties() { Properties prop = new Properties(); prop.put("hibernate.format_sql", "true"); prop.put("hibernate.show_sql", "true"); prop.put("hibernate.hbm2ddl.auto", "update"); prop.put("hibernate.dialect", "org.hibernate.dialect.MySQL5Dialect"); return prop; } @Bean(name = "dataSource") public BasicDataSource dataSource() { BasicDataSource ds = new BasicDataSource(); ds.setDriverClassName("com.mysql.jdbc.Driver"); ds.setUrl("jdbc:mysql://192.168.11.81:3306/security_learning_3"); ds.setUsername("petter"); ds.setPassword("petter"); return ds; } @Bean public HibernateTransactionManager txManager() { return new HibernateTransactionManager(sessionFactory()); } @Bean public SpringResourceTemplateResolver springResourceTemplateResolver() { SpringResourceTemplateResolver springResourceTemplateResolver = new SpringResourceTemplateResolver(); springResourceTemplateResolver.setPrefix("/WEB-INF/pages/"); springResourceTemplateResolver.setSuffix(".html"); springResourceTemplateResolver.setTemplateMode(TemplateMode.HTML); springResourceTemplateResolver.setCacheable(false); springResourceTemplateResolver.setCharacterEncoding("UTF-8"); return springResourceTemplateResolver; } @Bean public SpringTemplateEngine springTemplateEngine() { SpringTemplateEngine springTemplateEngine = new SpringTemplateEngine(); springTemplateEngine.setTemplateResolver(springResourceTemplateResolver()); springTemplateEngine.addDialect(new SpringSecurityDialect()); return springTemplateEngine; } @Bean public ThymeleafViewResolver thymeleafViewResolver() { ThymeleafViewResolver thymeleafViewResolver = new ThymeleafViewResolver(); thymeleafViewResolver.setTemplateEngine(springTemplateEngine()); thymeleafViewResolver.setCharacterEncoding("UTF-8"); return thymeleafViewResolver; } }
注意这里添加了事务注解@EnableTransactionManagement,否则运行会报无事务错误,并且需要在具体的repository中添加@Transactional注解
三、使用hibernate查询数据库获得User
1、定义接口UserDao
package com.petter.dao; import com.petter.model.User; /** * @author hongxf * @since 2017-04-17 10:41 */ public interface UserDao { User findByUserName(String username); }
2、实现接口UserDao
package com.petter.dao.impl; import com.petter.dao.UserDao; import com.petter.model.User; import org.hibernate.SessionFactory; import org.springframework.stereotype.Repository; import org.springframework.transaction.annotation.Transactional; import javax.annotation.Resource; import java.util.ArrayList; import java.util.List; /** * @author hongxf * @since 2017-04-17 10:42 */ @Repository public class UserDaoImpl implements UserDao { @Resource private SessionFactory sessionFactory; @SuppressWarnings("unchecked") @Transactional(readOnly = true) @Override public User findByUserName(String username) { List<User> users = sessionFactory.getCurrentSession() .createQuery("from User where username = ?") .setParameter(0, username) .list(); if (users.size() > 0) { return users.get(0); } else { return null; } } }
四、实现自定义UserDetailsService
package com.petter.service; import com.petter.dao.UserDao; import com.petter.model.User; import com.petter.model.UserRole; 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 javax.annotation.Resource; import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Set; /** * security的验证过程会调用指定的UserDetailsService * 自定义的UserDetailsService查询数据库得到自己User后 * 组装org.springframework.security.core.userdetails.User返回 * @author hongxf * @since 2017-04-17 10:45 */ @Service("userDetailsService") public class CustomUserDetailsService implements UserDetailsService { @Resource private UserDao userDao; @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { User user = userDao.findByUserName(username); if (user == null) { throw new UsernameNotFoundException("该用户不存在:" + username); } List<GrantedAuthority> authorities = buildUserAuthority(user.getUserRole()); return buildUserForAuthentication(user, authorities); } // 把自定义的User转换成org.springframework.security.core.userdetails.User private org.springframework.security.core.userdetails.User buildUserForAuthentication( User user, List<GrantedAuthority> authorities) { return new org.springframework.security.core.userdetails.User(user.getUsername(), user.getPassword(), user.isEnabled(), user.isAccountNonExpired(), user.isCredentialsNonExpired(), user.isAccountNonLocked(), authorities); } private List<GrantedAuthority> buildUserAuthority(Set<UserRole> userRoles) { Set<GrantedAuthority> setAuths = new HashSet<>(); // Build user's authorities for (UserRole userRole : userRoles) { setAuths.add(new SimpleGrantedAuthority(userRole.getRole())); } return new ArrayList<>(setAuths); } }
五、修改SecurityConfig配置
package com.petter.config; import com.petter.handler.CustomAuthenticationProvider; import com.petter.service.CustomUserDetailsService; 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.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.SavedRequestAwareAuthenticationSuccessHandler; import org.springframework.security.web.authentication.rememberme.JdbcTokenRepositoryImpl; import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository; import javax.annotation.Resource; import javax.sql.DataSource; /** * 相当于spring-security.xml中的配置 * @author hongxf * @since 2017-03-08 9:30 */ @Configuration @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { @Resource private DataSource dataSource; @Resource private CustomAuthenticationProvider authenticationProvider; @Resource private CustomUserDetailsService userDetailsService; @Bean public PasswordEncoder passwordEncoder(){ return new BCryptPasswordEncoder(); } @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { //authenticationProvider.setPasswordEncoder(passwordEncoder()); //auth.authenticationProvider(authenticationProvider); auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder()); } /** * 配置权限要求 * 采用注解方式,默认开启csrf * @param http * @throws Exception */ @Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests() .antMatchers("/admin/**").hasRole("ADMIN") .antMatchers("/dba/**").hasAnyRole("ADMIN", "DBA") .and() .formLogin().successHandler(savedRequestAwareAuthenticationSuccessHandler()) .loginPage("/login") //指定自定义登录页 .failureUrl("/login?error") //登录失败的跳转路径 .loginProcessingUrl("/auth/login_check") //指定了登录的form表单提交的路径,需与表单的action值保存一致,默认是login .usernameParameter("user-name").passwordParameter("pwd") .and() .logout().logoutSuccessUrl("/login?logout") .and() .exceptionHandling().accessDeniedPage("/403") .and() .csrf() .and() //.rememberMe().rememberMeParameter("remember-me") //其实默认就是remember-me,这里可以指定更换 //.tokenValiditySeconds(1209600) //.key("hongxf"); .rememberMe().tokenRepository(persistentTokenRepository()) .tokenValiditySeconds(1209600); } //如果采用持久化 token 的方法则需要指定保存token的方法 @Bean public PersistentTokenRepository persistentTokenRepository() { JdbcTokenRepositoryImpl db = new JdbcTokenRepositoryImpl(); db.setDataSource(dataSource); return db; } //使用remember-me必须指定UserDetailsService @Override protected UserDetailsService userDetailsService() { return userDetailsService; } /** * 这里是登录成功以后的处理逻辑 * 设置目标地址参数为targetUrl * /auth/login_check?targetUrl=/admin/update * 这个地址就会被解析跳转到/admin/update,否则就是默认页面 * * 本示例中访问update页面时候会判断用户是手动登录还是remember-me登录的 * 如果是remember-me登录的则会跳转到登录页面进行手动登录再跳转 * @return */ @Bean public SavedRequestAwareAuthenticationSuccessHandler savedRequestAwareAuthenticationSuccessHandler() { SavedRequestAwareAuthenticationSuccessHandler auth = new SavedRequestAwareAuthenticationSuccessHandler(); auth.setTargetUrlParameter("targetUrl"); return auth; } }
至此进行测试完全没有问题,但是此处的配置没有实现多次登录失败锁定用户的功能,因为这里没有指定自定义的AuthenticationProvider,使用的是默认的AuthenticationProvider的实现类DaoAuthenticationProvider。
于是使用自定义的AuthenticationProvider实现类CustomAuthenticationProvider
package com.petter.handler; import com.petter.dao.UserDetailsDao; import com.petter.model.UserAttempts; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.security.authentication.BadCredentialsException; import org.springframework.security.authentication.LockedException; import org.springframework.security.authentication.dao.DaoAuthenticationProvider; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.stereotype.Component; import javax.annotation.Resource; import java.util.Date; /** * 自定义验证程序,可以在这里进行其他附属操作,具体验证程序仍然调用上层接口即可 * @author hongxf * @since 2017-03-20 14:28 */ @Component("authenticationProvider") public class CustomAuthenticationProvider extends DaoAuthenticationProvider { @Resource private UserDetailsDao userDetailsDao; @Autowired @Qualifier("userDetailsService") @Override public void setUserDetailsService(UserDetailsService userDetailsService) { super.setUserDetailsService(userDetailsService); } @Override public Authentication authenticate(Authentication authentication) throws AuthenticationException { try { //调用上层验证逻辑 Authentication auth = super.authenticate(authentication); //如果验证通过登录成功则重置尝试次数, 否则抛出异常 userDetailsDao.resetFailAttempts(authentication.getName()); return auth; } catch (BadCredentialsException e) { //如果验证不通过,则更新尝试次数,当超过次数以后抛出账号锁定异常 userDetailsDao.updateFailAttempts(authentication.getName()); throw e; } catch (LockedException e){ //该用户已经被锁定,则进入这个异常 String error; UserAttempts userAttempts = userDetailsDao.getUserAttempts(authentication.getName()); if(userAttempts != null){ Date lastAttempts = userAttempts.getLastModified(); error = "用户已经被锁定,用户名 : " + authentication.getName() + "最后尝试登陆时间 : " + lastAttempts; }else{ error = e.getMessage(); } throw new LockedException(error); } } }
相应的使用hibernate修改之前的UserDetailsDao接口的实现方法
package com.petter.dao.impl; import com.petter.dao.UserDetailsDao; import com.petter.model.UserAttempts; import org.hibernate.SessionFactory; import org.springframework.dao.EmptyResultDataAccessException; import org.springframework.jdbc.core.support.JdbcDaoSupport; import org.springframework.security.authentication.LockedException; import org.springframework.stereotype.Repository; import org.springframework.transaction.annotation.Transactional; import javax.annotation.PostConstruct; import javax.annotation.Resource; import javax.sql.DataSource; import java.util.Date; import java.util.List; /** * @author hongxf * @since 2017-03-20 10:54 */ @Repository public class UserDetailsDaoImpl implements UserDetailsDao { @Resource private SessionFactory sessionFactory; private static final int MAX_ATTEMPTS = 3; @Transactional @Override public void updateFailAttempts(String username) { UserAttempts userAttempts = getUserAttempts(username); if (userAttempts == null) { if (isUserExists(username)) { //如果存在这个用户 // 如果之前没有记录,添加一条 userAttempts = new UserAttempts(); userAttempts.setUsername(username); userAttempts.setAttempts(1); userAttempts.setLastModified(new Date()); sessionFactory.getCurrentSession().save(userAttempts); } } else { if (isUserExists(username)) { userAttempts.setAttempts(userAttempts.getAttempts() + 1); userAttempts.setLastModified(new Date()); sessionFactory.getCurrentSession().update(userAttempts); sessionFactory.getCurrentSession().flush(); } if (userAttempts.getAttempts() >= MAX_ATTEMPTS) { // 大于尝试次数则锁定 sessionFactory.getCurrentSession() .createQuery("update User u set u.accountNonLocked =:accountNonLocked where u.username =:username") .setParameter("accountNonLocked", false) .setParameter("username", username) .executeUpdate(); // 并且抛出账号锁定异常 throw new LockedException("用户账号已被锁定,请联系管理员解锁"); } } } @SuppressWarnings("unchecked") @Transactional(readOnly = true) @Override public UserAttempts getUserAttempts(String username) { List<UserAttempts> list = sessionFactory.getCurrentSession() .createQuery("from UserAttempts where username =:username") .setParameter("username", username) .list(); if (list.size() > 0) { return list.get(0); } else { return null; } } @Transactional @Override public void resetFailAttempts(String username) { sessionFactory.getCurrentSession() .createQuery("update UserAttempts ua set ua.attempts = 0, ua.lastModified = null where ua.username =:username") .setParameter("username", username) .executeUpdate(); } /** * 判断用户是否存在 * @param username * @return */ private boolean isUserExists(String username) { boolean result = false; Long count = (Long) sessionFactory.getCurrentSession() .createQuery("select count(*) from User u where u.username =:username") .setParameter("username", username) .iterate().next(); if (count > 0) { result = true; } return result; } }
当然最后还要修改下SecurityConfig配置
@Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { authenticationProvider.setPasswordEncoder(passwordEncoder()); auth.authenticationProvider(authenticationProvider); //auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder()); }
指定自定义的authenticationProvider
其他有关的对应页面和Controlller参考之前的例子即可,运行测试。