Spring Boot整合Spring Security(4.0)入門(Maven)
之前沒有做過許可權方面的,組長讓我做下一版本的許可權管理,去看了幾篇部落格,感覺寫的不夠清晰,自己總結一下,僅當做自己學習的記錄(如有誤導、錯誤敬請諒解、指出)。
1.pom.xml中引入支援包:
<!-- springboot spring security支援包 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <dependency> <groupId>org.thymeleaf.extras</groupId> <artifactId>thymeleaf-extras-springsecurity4</artifactId> </dependency> <!-- nekohtml支援包,頁面可以使用不嚴格html5格式,即LEGACYHTML5 --> <dependency> <groupId>net.sourceforge.nekohtml</groupId> <artifactId>nekohtml</artifactId> </dependency>
thymeleaf-extras-springsecurity4這個支援包是必須要引入的,剛開始我不清楚它的作用沒有引入,後來發現它的作用是在頁面中可使用<div sec:authorize="hasRole('ROLE_USER')">有許可權才可以看見哦</div>這種許可權驗證表示式(《Spring實戰》中稱為方言),並不是在頁面中宣告<html lang="en" xmlns:th="http://www.thymeleaf.org" xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity4">才可以使用的。(引入支援包後頁面中聲不宣告都無所謂了,只不過聲明瞭在例如Eclipse等開發工具中頁面的sec:authorize="hasRole('ROLE_USER')"表示式會得到驗證,不再會得到警告資訊
2.application.properties中加入:
#預設html5,格式要求嚴格,LEGACYHTML5格式要求不嚴格(需要新增nokehtml的包依賴才能使用)
spring.thymeleaf.mode = LEGACYHTML5
在進入下面的程式碼編寫之前,首先看一下Spring Security的原理與流程:
原理:使用很多的攔截器對URL進行攔截,以此來管理登入驗證和使用者許可權驗證。
流程:使用者登陸,會被AuthenticationProcessingFilter攔截,呼叫AuthenticationManager的實現,而且AuthenticationManager會呼叫ProviderManager來獲取使用者驗證資訊(不同的Provider呼叫的服務不同,因為這些資訊可以是在資料庫上,可以是在LDAP伺服器上,可以是xml配置檔案上等),如果驗證通過後會將使用者的許可權資訊封裝成一個User放到spring的全域性快取SecurityContextHolder中,以備後面訪問資源時使用。
使用者名稱密碼->(Authentication(未認證) -> AuthenticationManager ->AuthenticationProvider->UserDetailsService->UserDetails->return Authentication(已認證,登入成功的token)
校驗原理:Spring Security 校驗的原理:左手配置資訊,右手登入後的使用者資訊,中間投票器。從我們的配置資訊中獲取相關的URL和需要的許可權資訊,然後獲得登入後的使用者資訊,然後經過:AccessDecisionManager 來驗證,這裡面有多個投票器:AccessDecisionVoter,(預設有幾種實現:比如:1票否決(只要有一個不同意,就沒有許可權),全票通過,才算通過;只要有1個通過,就全部通過。類似這種的。WebExpressionVoter 是Spring Security預設提供的的web開發的投票器。(表示式的投票器)Spring Security 預設的是 AffirmativeBased 只要有一個通過,就通過。
SpringBoot中,預設的Spring Security就是生效的。(即引入支援包就會生效了)
3.自己的web安全配置,配置哪些頁面訪問需要登入、登入頁面、登入成功處理者、登入失敗處理者等等:
package www.yzh.com.configuration;
import org.springframework.beans.factory.annotation.Autowired;
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 www.yzh.com.component.MyAuthenticationProvider;
/**
* 我的安全配置
* Title: MySecurityConfig
* Description:
* @author yzh
* @date 2018年4月26日上午11:44:17
*/
@Configuration
@EnableWebSecurity//@EnableWebMvcSecurity 註解開啟Spring Security的驗證
public class MyWebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
MyAuthenticationProvider myAuthenticationProvider;
@Autowired
MyAuthenticationSuccessHandler myAuthenticationSuccessHandler;
@Autowired
MyAuthenticationFailureHandler myAuthenticationFailureHandler;
//通過過載,配置如何通過攔截器保護請求
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/hello","/hi").permitAll()//hello,hi請求允許任何人訪問(必須配置在.anyRequest().authenticated()之前)
.antMatchers("/testrole").hasRole("smalladmin")//有smalladmin的role才能訪問
.anyRequest()//.access(attribute)//自定義驗證
.authenticated()//其它所有請求都需要驗證
.and().formLogin().loginPage("/login.html")//登入頁面
.loginProcessingUrl("/login")//登入介面
.successHandler(myAuthenticationSuccessHandler)//登陸成功自定義處理
.failureUrl("/login?error")//登入失敗介面
.failureHandler(myAuthenticationFailureHandler)//登陸失敗處理
.permitAll()//允許所有請求(不需要驗證)
.and().logout().permitAll()//登出(不需要驗證)
.and().rememberMe()//開啟cookie儲存使用者資料
.and().csrf().disable();//關閉csrf(跨站請求偽造)防護,@EnableWebSecurity預設為表單開啟了;若開啟,<form>改為<form th:action="@{/hello}">就可為表單自動生成一個"_csrf"的隱藏域token用於驗證
}
//配置UserDetail服務
@Override
protected void configure(AuthenticationManagerBuilder auth)throws Exception {
//super.configure(auth);
//密碼加密器,使用自定義認證提供者之後不再需要配置
//auth.userDetailsService(securityCmpt).passwordEncoder(passwordEncoder());
//讓自己的認證提供者生效
auth.authenticationProvider(myAuthenticationProvider);
}
// /**
// * 配置加密器
// * @return
// */
// @Bean
// public PasswordEncoder passwordEncoder(){
// return new PasswordEncoder() {
// @Override
// public String encode(CharSequence rawPassword) {
// return DigestUtils.sha256Hex(DigestUtils.md5Hex(rawPassword.toString()));
// }
//
// @Override
// public boolean matches(CharSequence rawPassword, String encodedPassword) {
// //先md5加密 再sha256加密
// return encode(rawPassword).equals(encodedPassword);
// }
// };
// }
}
4.使用者詳細服務,用來獲取(從資料庫中)封裝(程式碼中)使用者資訊:
package www.yzh.com.component;
import org.springframework.beans.factory.annotation.Autowired;
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.Component;
import www.yzh.com.dao.AdminDao;
import www.yzh.com.entity.Admin;
import www.yzh.com.entity.AdminDto;
/**
* 使用者詳細服務
* Title: MyUserDetailsService
* Description:
* @author yzh
* @date 2018年4月26日下午12:38:19
*/
@Component
public class MyUserDetailsService implements UserDetailsService {
@Autowired
AdminDao adminDao;
/**
* 這裡返回的UserDetails是為了和使用者輸入的賬號進行比較(並不是返回的結果就是登陸是否成功的標識)
*/
@Override
public UserDetails loadUserByUsername(String username)throws UsernameNotFoundException {
Admin admin = adminDao.selectByName(username);
if(admin != null){
AdminDto adminDto = new AdminDto(admin.getName(), admin.getPassword(), admin.getRoleId(), "ROLE_bigadmin", true, true, true, true);
return adminDto;
}
return null;
}
}
其中Admin:
package www.yzh.com.entity;
public class Admin extends BaseEntity{
private Integer id;
private String name;
private String password;
private Integer roleId;
private static final long serialVersionUID = 1L;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name == null ? null : name.trim();
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password == null ? null : password.trim();
}
public Integer getRoleId() {
return roleId;
}
public void setRoleId(Integer roleId) {
this.roleId = roleId;
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append(getClass().getSimpleName());
sb.append(" [");
sb.append("Hash = ").append(hashCode());
sb.append(", id=").append(id);
sb.append(", name=").append(name);
sb.append(", password=").append(password);
sb.append(", roleId=").append(roleId);
sb.append(", serialVersionUID=").append(serialVersionUID);
sb.append("]");
return sb.toString();
}
public Admin() {
super();
}
public Admin(Integer id, String name, String password, Integer roleId) {
super();
this.id = id;
this.name = name;
this.password = password;
this.roleId = roleId;
}
}
其中AdminDto:
package www.yzh.com.entity;
import java.util.Collection;
import java.util.List;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.userdetails.UserDetails;
public class AdminDto extends Admin implements UserDetails{
private String name;
private String password;
private Integer roleId;
private String role;
private boolean accountNonExpired;
private boolean accountNonLocked;
private boolean credentialsNonExpired;
private boolean enabled;
private static final long serialVersionUID = 1L;
public AdminDto( String name, String password, Integer roleId,
String role, boolean accountNonExpired, boolean accountNonLocked,
boolean credentialsNonExpired, boolean enabled) {
super();
this.name = name;
this.password = password;
this.roleId = roleId;
this.role = role;
this.accountNonExpired = accountNonExpired;
this.accountNonLocked = accountNonLocked;
this.credentialsNonExpired = credentialsNonExpired;
this.enabled = enabled;
}
public String getRole() {
return role;
}
public void setRole(String role) {
this.role = role;
}
public void setAccountNonExpired(boolean accountNonExpired) {
this.accountNonExpired = accountNonExpired;
}
public void setAccountNonLocked(boolean accountNonLocked) {
this.accountNonLocked = accountNonLocked;
}
public void setCredentialsNonExpired(boolean credentialsNonExpired) {
this.credentialsNonExpired = credentialsNonExpired;
}
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name == null ? null : name.trim();
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password == null ? null : password.trim();
}
public Integer getRoleId() {
return roleId;
}
public void setRoleId(Integer roleId) {
this.roleId = roleId;
}
@Override
public String toString() {
return "AdminDto [name=" + name + ", password="
+ password + ", roleId=" + roleId + ", role=" + role
+ ", accountNonExpired=" + accountNonExpired
+ ", accountNonLocked=" + accountNonLocked
+ ", credentialsNonExpired=" + credentialsNonExpired
+ ", enabled=" + enabled + "]";
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
// TODO Auto-generated method stub
List<GrantedAuthority> list = AuthorityUtils.commaSeparatedStringToAuthorityList(role);
return list;
}
@Override
public String getUsername() {
// TODO Auto-generated method stub
return name;
}
@Override
public boolean isAccountNonExpired() {
// TODO Auto-generated method stub
return accountNonExpired;
}
@Override
public boolean isAccountNonLocked() {
// TODO Auto-generated method stub
return accountNonLocked;
}
@Override
public boolean isCredentialsNonExpired() {
// TODO Auto-generated method stub
return credentialsNonExpired;
}
@Override
public boolean isEnabled() {
// TODO Auto-generated method stub
return enabled;
}
}
5.自己的認證提供者,驗證使用者名稱密碼是否正確以及錯誤資訊提示,返回登入成功的認證標識Authentication:
package www.yzh.com.component;
import java.util.Collection;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Component;
/**
* 自己的認證提供者
* Title: MyAuthenticationProvider
* Description:
* @author yzh
* @date 2018年4月26日下午5:19:57
*/
@Component
public class MyAuthenticationProvider implements AuthenticationProvider {
@Autowired
MyUserDetailsService myUserDetailsService;
@Override
public Authentication authenticate(Authentication authentication)throws AuthenticationException {
String name = (String) authentication.getPrincipal();
String password = (String) authentication.getCredentials();
UserDetails userDetails = myUserDetailsService.loadUserByUsername(name);
if(userDetails == null){
throw new UsernameNotFoundException("使用者名稱不存在");
}
if(!userDetails.getPassword().equals(password)){
throw new BadCredentialsException("密碼不正確");
}
Collection<? extends GrantedAuthority> authorities = userDetails.getAuthorities();
//構建返回使用者登入成功的token
UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(userDetails, password, authorities);
return token;
}
@Override
public boolean supports(Class<?> authentication) {
// TODO Auto-generated method stub
//表示支援
return true;
}
}
6.我的認證成功後的處理:
package www.yzh.com.configuration;
import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.DefaultRedirectStrategy;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler;
import org.springframework.stereotype.Component;
import com.fasterxml.jackson.databind.ObjectMapper;
/**
* 我的認證成功後的處理
* Title: MyAuthenticationSuccessHandler
* Description:
* @author yzh
* @date 2018年4月27日上午11:03:54
*/
@Component(value="myAuthenticationSuccessHandler")
public class MyAuthenticationSuccessHandler extends SimpleUrlAuthenticationSuccessHandler {
@Autowired
ObjectMapper objectMapper;
@Override
public void onAuthenticationSuccess(HttpServletRequest request,
HttpServletResponse response, Authentication authentication)
throws IOException, ServletException {
//登陸成功後自己的業務邏輯
System.out.println("登陸成功");
//返回json
// response.setContentType("application/json;charset=UTF-8");
// response.getWriter().write(objectMapper.writeValueAsString(authentication));
//如果是要跳轉到某個頁面
new DefaultRedirectStrategy().sendRedirect(request, response, "/index");
}
}
7.我的認證失敗後的處理:
package www.yzh.com.configuration;
import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.DefaultRedirectStrategy;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler;
import org.springframework.stereotype.Component;
import com.fasterxml.jackson.databind.ObjectMapper;
/**
* 我的認證失敗後的處理
* Title: MyAuthenticationSuccessHandler
* Description:
* @author yzh
* @date 2018年4月27日上午11:03:54
*/
@Component(value="myAuthenticationFailureHandler")
public class MyAuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler {
@Autowired
ObjectMapper objectMapper;
@Override
public void onAuthenticationFailure(HttpServletRequest request,
HttpServletResponse response, AuthenticationException exception)
throws IOException, ServletException {
// TODO Auto-generated method stub
// super.onAuthenticationFailure(request, response, exception);
System.out.println("登陸失敗");
response.setContentType("application/json;charset=utf-8");
response.getWriter().write(objectMapper.writeValueAsString(exception.getMessage()));
}
}
8.頁面及控制層程式碼:
登入頁面:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8"></meta>
<title>Insert title here</title>
</head>
<body>
<form class="form-signin" action="/login" method="post">
<h2>自定義登入頁面</h2>
<table>
<tr>
<td>使用者名稱:</td>
<td><input type="text" name="username" class="form-control" placeholder="請輸入使用者名稱"></td>
</tr>
<tr>
<td>密碼:</td>
<td><input type="password" name="password" class="form-control" placeholder="請輸入密碼" /></td>
</tr>
<tr>
<td colspan="2">
<button type="submit" class="btn btn-lg btn-primary btn-block" >登入</button>
</td>
</tr>
</table>
</form>
</body>
</html>
登入成功頁面:
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity4">
<head>
<meta charset="UTF-8"></meta>
<title>Insert title here</title>
</head>
<body>
index 你能看見我嗎?
<div sec:authorize="hasRole('bigadmina')">
<p class="bg-info">有更多顯示資訊</p>
</div>
<div th:text="${principal}"></div>
<form class="form-signin" action="/logout" method="post">
<input type="submit" value="登出" />
</form>
<a sec:authorize="hasRole('bigadmina')" href="/hi">hi</a>
<a href="/testrole">testrole</a>
</body>
<script type="text/javascript" th:inline="javascript">
</script>
</html>
控制層程式碼:
package www.yzh.com.controller;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
@Controller
public class HelloSecurityController {
@RequestMapping("/hello")
public String hello(Model model){
return "hello";
}
@RequestMapping("/hi")
public String hi(Model model){
return "hello";
}
@RequestMapping("/testrole")
public String testrole(Model model){
return "hello";
}
@RequestMapping("/")
public String success(Model model){
return "redirect:/index";
}
@RequestMapping("/index")
public String index(Model model){
Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
model.addAttribute("authentication", authentication);
Object credentials = authentication.getCredentials();
model.addAttribute("credentials", credentials);
model.addAttribute("principal", principal);
return "index";
}
@RequestMapping("/login")
public String login(Model model,String error){
model.addAttribute("error", error);
return "login";
}
}
後端程式碼中,許可權資訊,使用者資訊,角色資訊都應該從資料庫中獲取就不再展示程式碼。