本文主要介紹在Spring Boot中整合Spring Security,對於Spring Boot配置及使用不做過多介紹,還不了解的同學可以先學習下Spring Boot。
本demo所用Spring Boot版本為2.1.4.RELEASE。
1、pom.xml中增加依賴
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency>
2、Spring Security配置類
package com.inspur.webframe.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.core.userdetails.UserDetailsService; import com.inspur.webframe.security.UserDetailsServiceImpl; @Configuration public class SecurityConfig extends WebSecurityConfigurerAdapter { @Bean @Override protected UserDetailsService userDetailsService() { return new UserDetailsServiceImpl(); } @Override protected void configure(HttpSecurity http) throws Exception { http .authorizeRequests().anyRequest().authenticated() .and() .csrf().disable() //禁用csrf .headers().frameOptions().disable() //禁用frame options .and() .formLogin().loginPage("/demo/login").loginProcessingUrl("/j_spring_security_check").failureUrl("/demo/login?error=true").defaultSuccessUrl("/demo/main").permitAll() .and() .logout().logoutUrl("/j_spring_security_logout").logoutSuccessUrl("/demo/login").permitAll(); } }
userDetailsService返回自己實現的UserDetailsService,見下面UserDetailsServiceImpl類。
configure方法中配置了如下內容:
登錄頁面url:/demo/login
登錄處理url:/j_spring_security_check,對應登錄頁面中登錄操作url
登錄失敗url:/demo/login?error=true
登錄成功url:/demo/main
注銷url:/j_spring_security_logout,對應歡迎頁面中注銷操作url
注銷成功跳轉url:/demo/login,調到登錄頁面
3、用戶類
該類與數據庫的用戶表對應
package com.inspur.webframe.security; import java.io.Serializable; import com.inspur.common.entity.BaseEntity; public class User extends BaseEntity implements Serializable { private static final long serialVersionUID = 1L; /** * 用戶id */ private String userid; /** * 用戶密碼 */ private String password; /** * 用戶名 */ private String username; /** * 是否被鎖定 1:是 0:否 */ private Integer isLocked; public Integer getIsLocked() { return isLocked; } public void setIsLocked(Integer isLocked) { this.isLocked = isLocked; } public String getUserid() { return userid; } public void setUserid(String userid) { this.userid = userid; } public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } @Override public String toString() { return "User [userid=" + userid + ", password=" + password + ", username=" + username + "]"; } }
BaseEntity是一個基類,有id、創建時間、修改時間等基礎信息,作為demo可以忽略
4、自定義UserDetails
該類需要實現org.springframework.security.core.userdetails.UserDetails接口,作為用戶信息;該類關聯用戶類
package com.inspur.webframe.security; import java.util.Collection; import org.apache.commons.codec.binary.Base64; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.userdetails.UserDetails; import com.inspur.common.util.EncoderUtil; public class SecurityUser implements UserDetails { private static final long serialVersionUID = 4118167338060103803L; private User systemUser = null; private Collection<? extends GrantedAuthority> authorities = null; public SecurityUser(User systemUser, Collection<? extends GrantedAuthority> authorities) { this.systemUser = systemUser; this.authorities = authorities; } public User getSystemUser() { return systemUser; } @Override public Collection<? extends GrantedAuthority> getAuthorities() { return authorities; } @Override public String getPassword() { //{MD5}e10adc3949ba59abbe56e057f20f883e,123456
return systemUser.getPassword(); } @Override public String getUsername() { return systemUser.getUserid(); } @Override public boolean isAccountNonExpired() { return true; } @Override public boolean isAccountNonLocked() { return !(systemUser.getIsLocked() != null && systemUser.getIsLocked() == 1); } @Override public boolean isCredentialsNonExpired() { return true; } @Override public boolean isEnabled() { return true; } }
spring security 有多種驗證密碼算法,這里使用的MD5算法,格式為:{MD5}e10adc3949ba59abbe56e057f20f883e;如果數據保存的密碼格式不是這種格式,可以在getPassword()方法中轉換成標准格式。
5、自定義UserDetailsService
該類需要實現org.springframework.security.core.userdetails.UserDetailsService接口,用於用戶的登錄認證
package com.inspur.webframe.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 com.inspur.common.dao.BaseDao; public class UserDetailsServiceImpl implements UserDetailsService { protected static Logger logger = LoggerFactory.getLogger(UserDetailsServiceImpl.class); @Autowired private BaseDao baseDao; @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { logger.info(username); User user = baseDao.selectForObject(User.class, "userid=?", username); logger.info("user={}", user); if (user != null) { //權限,應從數據取這里寫死 List<GrantedAuthority> authorities= new ArrayList<GrantedAuthority>(); authorities.add(new SimpleGrantedAuthority("ROLE_USER")); authorities.add(new SimpleGrantedAuthority("ROLE_ADMIN")); SecurityUser u = new SecurityUser(user, authorities); logger.info(u.getPassword()); return u; } throw new UsernameNotFoundException("用戶(" + username + ")不存在"); } }
baseDao是我實現的操作數據庫的工具類,類似spring的jdbcTemplate;不是重點,具體實現細節就不貼出來了,看代碼也能看出意思
6、訪問url的controller
package com.inspur.demo.web.controller; import org.springframework.security.core.Authentication; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import com.inspur.common.web.controller.BaseController; @Controller @RequestMapping(method = {RequestMethod.GET, RequestMethod.POST}) public class DemoController extends BaseController { @RequestMapping(value={"/demo/login", "/"}) public String login() { return "/demo/login"; } @RequestMapping(value={"/demo/main"}) public String main(Authentication authentication) { logger.info("authentication.getPrincipal()={}", authentication.getPrincipal()); return "/demo/main"; } }
7、thymeleaf
thymeleaf是spring推薦使用的模板引擎,可以優雅的來畫頁面。
- 引入依賴
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency>
- 配置application.yml(或application.properties)
spring: resources: static-locations: classpath:/static/ thymeleaf: encoding: utf-8 cache: false
8、編寫頁面login.html
頁面位置為/src/main/resources/templates/demo/login.html
<!DOCTYPE HTML> <html> <head> <title>My JSP 'login.jsp' starting page</title> <meta http-equiv="pragma" content="no-cache"> <meta http-equiv="cache-control" content="no-cache"> <meta http-equiv="expires" content="0"> </head> <body> <form th:action="@{/j_spring_security_check}" method="post"> <input type="hidden" name ="${_csrf.parameterName}" value ="${_csrf.token}" /> <table> <tr> <td> 用戶名:</td> <td><input type="text" name="username"/></td> </tr> <tr> <td> 密碼:</td> <td><input type="password" name="password"/></td> </tr> <tr> <td> <span style="color: red;" th:if="${param.error != null && session.SPRING_SECURITY_LAST_EXCEPTION != null }" th:text="${session.SPRING_SECURITY_LAST_EXCEPTION.message}"></span> </td> </tr> <tr> <td colspan="2" align="center"> <input type="submit" value=" 登錄 "/> <input type="reset" value=" 重置 "/> </td> </tr> </table> </form> </body> </html>
/j_spring_security_check對應上面SecurityConfig配置的登錄路徑;session.SPRING_SECURITY_LAST_EXCEPTION.message表示登錄錯誤的信息。
9、編寫頁面main.html
頁面位置為/src/main/resources/templates/demo/main.html
<!DOCTYPE HTML> <html> <head> <title>My JSP 'main.jsp' starting page</title> <meta http-equiv="pragma" content="no-cache"> <meta http-equiv="cache-control" content="no-cache"> <meta http-equiv="expires" content="0"> </head> <body> 歡迎! <a th:href="@{/j_spring_security_logout}">退出</a> </body> </html>
/j_spring_security_check對應上面SecurityConfig配置的注銷路徑
10、測試
訪問登錄頁面http://localhost:8080/webframe/demo/login,我的server.servlet.context-path配置為/webframe
登錄失敗:
登錄成功:
11、擴展功能-鎖定用戶
簡單實現:用戶表中需要有is_locked(是否鎖定)、login_fail_times(連續登錄失敗次數)這兩個字段;連續登錄失敗次數超過一定值就鎖定用戶。
增加listener監聽登錄事件。
package com.inspur.webframe.security; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ApplicationListener; import org.springframework.security.authentication.event.AbstractAuthenticationEvent; import org.springframework.security.authentication.event.AuthenticationFailureBadCredentialsEvent; import org.springframework.security.authentication.event.AuthenticationSuccessEvent; import org.springframework.stereotype.Component; import com.inspur.common.dao.BaseDao; @Component public class LoginListener implements ApplicationListener<AbstractAuthenticationEvent> { private static Logger logger = LoggerFactory.getLogger(LoginListener.class); private static int MAX_FAIL_TIMES = 3; @Autowired private BaseDao baseDao; @Override public void onApplicationEvent(AbstractAuthenticationEvent event) { logger.info(event.getClass().toString()); String userId = event.getAuthentication().getName(); if (event instanceof AuthenticationSuccessEvent) { baseDao.update("update a_hr_userinfo set login_fail_times=0 where userid=? and login_fail_times>0", userId); } else if (event instanceof AuthenticationFailureBadCredentialsEvent) { baseDao.update("update a_hr_userinfo set login_fail_times=login_fail_times+1 where userid=?", userId); baseDao.update("update a_hr_userinfo set is_locked=1 where userid=? and login_fail_times>=?", userId, MAX_FAIL_TIMES); } } }
登錄成功login_fail_times清0,登錄失敗login_fail_times加1,到達3就鎖定用戶;這邊也用到了baseDao,具體意思看代碼也能明白。