本節是在基於注解方式進行的,后面的例子都會基於注解形式,不再實現XML配置形式,畢竟注解才是趨勢嘛!
關鍵在於實現自定義的UserDetailsService和AuthenticationProvider
項目結構如下:

查看spring security的源代碼可以發現默認security已經定義的user中的一些變量,鑒於此創建users表如下:
CREATE TABLE users ( username VARCHAR(45) NOT NULL, password VARCHAR(45) NOT NULL, enabled BOOLEAN NOT NULL DEFAULT TRUE, accountNonExpired BOOLEAN NOT NULL DEFAULT TRUE, accountNonLocked BOOLEAN NOT NULL DEFAULT TRUE, credentialsNonExpired BOOLEAN NOT NULL DEFAULT TRUE, PRIMARY KEY (username) );
用戶角色表user_roles:
CREATE TABLE user_roles ( user_role_id int(11) NOT NULL AUTO_INCREMENT, username varchar(45) NOT NULL, role varchar(45) NOT NULL, PRIMARY KEY (user_role_id), UNIQUE KEY uni_username_role (role,username), KEY fk_username_idx (username), CONSTRAINT fk_username FOREIGN KEY (username) REFERENCES users (username) );
用戶嘗試登陸次數表user_attempts:
CREATE TABLE user_attempts ( id int(11) NOT NULL AUTO_INCREMENT, username varchar(45) NOT NULL, attempts varchar(45) NOT NULL, lastModified datetime, PRIMARY KEY (id) );
插入數據:
INSERT INTO users(username,password,enabled) VALUES ('hxf','123456', true); INSERT INTO users(username,password,enabled) VALUES ('wpp','123456', true); INSERT INTO user_roles (username, role) VALUES ('hxf', 'ROLE_USER'); INSERT INTO user_roles (username, role) VALUES ('hxf', 'ROLE_ADMIN'); INSERT INTO user_roles (username, role) VALUES ('wpp', 'ROLE_USER');
一、用戶嘗試次數類以及相關的操作類
對應
user_attempts 表的UserAttempts
package com.petter.model; import java.util.Date; /** * @author hongxf * @since 2017-03-20 10:50 */ public class UserAttempts { private int id; private String username; private int attempts; private Date lastModified; public int getId() { return id; } public void setId(int id) { this.id = id; } public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } public int getAttempts() { return attempts; } public void setAttempts(int attempts) { this.attempts = attempts; } public Date getLastModified() { return lastModified; } public void setLastModified(Date lastModified) { this.lastModified = lastModified; } }
對應的操作類,接口UserDetailsDao:
package com.petter.dao; import com.petter.model.UserAttempts; /** * @author hongxf * @since 2017-03-20 10:53 */ public interface UserDetailsDao { void updateFailAttempts(String username); void resetFailAttempts(String username); UserAttempts getUserAttempts(String username); }
其實現類UserDetailsDaoImpl 如下,具體見注釋:
package com.petter.dao.impl; import com.petter.dao.UserDetailsDao; import com.petter.model.UserAttempts; import org.springframework.dao.EmptyResultDataAccessException; import org.springframework.jdbc.core.support.JdbcDaoSupport; import org.springframework.security.authentication.LockedException; import org.springframework.stereotype.Repository; import javax.annotation.PostConstruct; import javax.annotation.Resource; import javax.sql.DataSource; import java.util.Date; /** * @author hongxf * @since 2017-03-20 10:54 */ @Repository public class UserDetailsDaoImpl extends JdbcDaoSupport implements UserDetailsDao { private static final String SQL_USERS_UPDATE_LOCKED = "UPDATE USERS SET accountNonLocked = ? WHERE username = ?"; private static final String SQL_USERS_COUNT = "SELECT count(*) FROM USERS WHERE username = ?"; private static final String SQL_USER_ATTEMPTS_GET = "SELECT * FROM USER_ATTEMPTS WHERE username = ?"; private static final String SQL_USER_ATTEMPTS_INSERT = "INSERT INTO USER_ATTEMPTS (USERNAME, ATTEMPTS, LASTMODIFIED) VALUES(?,?,?)"; private static final String SQL_USER_ATTEMPTS_UPDATE_ATTEMPTS = "UPDATE USER_ATTEMPTS SET attempts = attempts + 1, lastmodified = ? WHERE username = ?"; private static final String SQL_USER_ATTEMPTS_RESET_ATTEMPTS = "UPDATE USER_ATTEMPTS SET attempts = 0, lastmodified = null WHERE username = ?"; private static final int MAX_ATTEMPTS = 3; @Resource private DataSource dataSource; @PostConstruct private void initialize() { setDataSource(dataSource); } @Override public void updateFailAttempts(String username) { UserAttempts user = getUserAttempts(username); if (user == null) { if (isUserExists(username)) { // 如果之前沒有記錄,添加一條 getJdbcTemplate().update(SQL_USER_ATTEMPTS_INSERT, username, 1, new Date()); } } else { if (isUserExists(username)) { // 存在用戶則失敗一次增加一次嘗試次數 getJdbcTemplate().update(SQL_USER_ATTEMPTS_UPDATE_ATTEMPTS, new Date(), username); } if (user.getAttempts() + 1 >= MAX_ATTEMPTS) { // 大於嘗試次數則鎖定 getJdbcTemplate().update(SQL_USERS_UPDATE_LOCKED, false, username); // 並且拋出賬號鎖定異常 throw new LockedException("用戶賬號已被鎖定,請聯系管理員解鎖"); } } } @Override public UserAttempts getUserAttempts(String username) { try { UserAttempts userAttempts = getJdbcTemplate().queryForObject(SQL_USER_ATTEMPTS_GET, new Object[] { username }, (rs, rowNum) -> { UserAttempts user = new UserAttempts(); user.setId(rs.getInt("id")); user.setUsername(rs.getString("username")); user.setAttempts(rs.getInt("attempts")); user.setLastModified(rs.getDate("lastModified")); return user; }); return userAttempts; } catch (EmptyResultDataAccessException e) { return null; } } @Override public void resetFailAttempts(String username) { getJdbcTemplate().update( SQL_USER_ATTEMPTS_RESET_ATTEMPTS, username); } private boolean isUserExists(String username) { boolean result = false; int count = getJdbcTemplate().queryForObject( SQL_USERS_COUNT, new Object[] { username }, Integer.class); if (count > 0) { result = true; } return result; } }
二、實現自定義的UserDetailsService
由於使用的jdbc方式查詢數據庫,spring以及幫我們實現了一個UserDetailsService,就是JdbcDaoImpl,查看源代碼
package org.springframework.security.core.userdetails.jdbc; public class JdbcDaoImpl extends JdbcDaoSupport implements UserDetailsService { //... protected List<UserDetails> loadUsersByUsername(String username) { return getJdbcTemplate().query(usersByUsernameQuery, new String[] {username}, new RowMapper<UserDetails>() { public UserDetails mapRow(ResultSet rs, int rowNum) throws SQLException { String username = rs.getString(1); String password = rs.getString(2); boolean enabled = rs.getBoolean(3); return new User(username, password, enabled, true, true, true, AuthorityUtils.NO_AUTHORITIES); } }); }
可見已經實現了UserDetailsService,但是它默認設置accountNonLocked總是true,我們在此基礎上進行實現 CustomUserDetailsService
package com.petter.service; import org.springframework.beans.factory.annotation.Value; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.AuthorityUtils; import org.springframework.security.core.userdetails.User; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.jdbc.JdbcDaoImpl; import org.springframework.stereotype.Service; import javax.annotation.PostConstruct; import javax.annotation.Resource; import javax.sql.DataSource; import java.util.List; /** * 查看JdbcDaoImpl的源碼可以發現這是實現自定義的UserDetailsService * 添加上鎖定和過期信息 * @author hongxf * @since 2017-03-20 12:30 */ @Service("userDetailsService") public class CustomUserDetailsService extends JdbcDaoImpl { @Resource private DataSource dataSource; @PostConstruct private void initialize() { setDataSource(dataSource); } @Override @Value("select * from users where username = ?") public void setUsersByUsernameQuery(String usersByUsernameQueryString) { super.setUsersByUsernameQuery(usersByUsernameQueryString); } @Override @Value("select username, role from user_roles where username = ?") public void setAuthoritiesByUsernameQuery(String queryString) { super.setAuthoritiesByUsernameQuery(queryString); } @Override protected List<UserDetails> loadUsersByUsername(String username) { return getJdbcTemplate().query(super.getUsersByUsernameQuery(), new Object[]{username}, (rs, rowNum) -> { String username1 = rs.getString("username"); String password = rs.getString("password"); boolean enabled = rs.getBoolean("enabled"); boolean accountNonExpired = rs.getBoolean("accountNonExpired"); boolean credentialsNonExpired = rs.getBoolean("credentialsNonExpired"); boolean accountNonLocked = rs.getBoolean("accountNonLocked"); return new User(username1, password, enabled, accountNonExpired, credentialsNonExpired, accountNonLocked, AuthorityUtils.NO_AUTHORITIES); }); } @Override protected UserDetails createUserDetails(String username, UserDetails userFromUserQuery, List<GrantedAuthority> combinedAuthorities) { String returnUsername = userFromUserQuery.getUsername(); if (!super.isUsernameBasedPrimaryKey()) { returnUsername = username; } return new User(returnUsername, userFromUserQuery.getPassword(), userFromUserQuery.isEnabled(), userFromUserQuery.isAccountNonExpired(), userFromUserQuery.isCredentialsNonExpired(), userFromUserQuery.isAccountNonLocked(), combinedAuthorities); } }
三、實現自定義的AuthenticationProvider,當每次登錄失敗以后更新用戶嘗試次數表
我們仍然可以繼承一個類DaoAuthenticationProvider來快速實現
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 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); } } }
四、根據拋出的異常實現自定義錯誤信息
修改登錄的方法,我們獲取session存儲的SPRING_SECURITY_LAST_EXCEPTION的值,自定義錯誤信息
//獲取session存儲的SPRING_SECURITY_LAST_EXCEPTION的值,自定義錯誤信息 @RequestMapping(value = "/login", method = RequestMethod.GET) public ModelAndView login( @RequestParam(value = "error", required = false) String error, @RequestParam(value = "logout", required = false) String logout, HttpServletRequest request) { ModelAndView model = new ModelAndView(); if (error != null) { model.addObject("error", getErrorMessage(request, "SPRING_SECURITY_LAST_EXCEPTION")); } if (logout != null) { model.addObject("msg", "你已經成功退出"); } model.setViewName("login"); return model; } //自定義錯誤類型 private String getErrorMessage(HttpServletRequest request, String key){ Exception exception = (Exception) request.getSession().getAttribute(key); String error; if (exception instanceof BadCredentialsException) { error = "不正確的用戶名或密碼"; }else if(exception instanceof LockedException) { error = exception.getMessage(); }else{ error = "不正確的用戶名或密碼"; } return error; }
五、最后配置自定義的驗證類CustomAuthenticationProvider
修改SecurityConfig
package com.petter.config; import com.petter.handler.CustomAuthenticationProvider; 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 javax.annotation.Resource; /** * 相當於spring-security.xml中的配置 * @author hongxf * @since 2017-03-08 9:30 */ @Configuration @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { @Resource private CustomAuthenticationProvider authenticationProvider; @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.authenticationProvider(authenticationProvider); } /** * 配置權限要求 * 采用注解方式,默認開啟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().loginPage("/login") .defaultSuccessUrl("/welcome").failureUrl("/login?error") .usernameParameter("user-name").passwordParameter("pwd") .and() .logout().logoutSuccessUrl("/login?logout") .and() .exceptionHandling().accessDeniedPage("/403") .and() .csrf(); } }
啟動程序進行測試,測試時候賬號必須是數據庫存在的,然后嘗試失敗3次,賬號即被鎖定