spring boot集成security
git地址
https://github.com/a18792721831/studySpringCloud.git
1. security介紹
Spring Security 是Spring Resource 社區的一個安全組件.Sping Secuity爲JavaEE企業級開發提供了全面的安全防護,安全防護是一個不斷變化的目標,Spring Security通過版本不斷迭代來實現這一目標。Spine Sceunt採用"安全層”的概念,使每一層都儘可能安全,連續的安全層可以達到全面的防護。Spring Security可以在Contoller層、Service層,DAO層等以加註解的方式來保護應用程序的安全,Spring Security 提供了細粒度的權限控制,可以精細到每一個API接口、每一個業務的方法,或者每一個操作數據庫的DAO層的方法.Spring Security提供的是應用程序層的安全解決方案,一個系統的安全還需要考患傳輸層和系統層的安全,例如採用Htpps協議、服務器部署防火牆等。
2 爲什麼選擇 Spring Security
使用 Spring Securiy有很多原因,其中一個重要原因是它對環境的無依賴性、低代碼耦合性。將工程重現部署到一個新的服務器上,不需要爲 Spring Security做什麼工作。Spring Security 提供了數十個安全模塊,模塊與模塊間的耦合性低,模塊之間可以自由組合來實現特定需求的安全功能,具有較高的可定製性。總而言之,Spring Security 具有很好的可複用性和可定製性。
在安全方面,有兩個主要的領域,一是“認證”,即你是誰;二是“授權”,即你擁有什麼權限,Spring Security 的主要目標就是在這兩個領域。“認證”是認證主體的過程,通常是指可以在應用程序中執行操作的用戶、設備或其他系統。“授權”是指決定是否允許已認證的主體執行某一項操作。
安全框架多種多樣,那爲什麼選擇 Spring Security 作爲微服務開發的安全框架呢?JavaEE 有另一個優秀的安全框架 Apache Shiro,Apache Shiro 在企業級的項目開發中十分受歡迎,一般使用在單體服務中。但在微服務架構中,目前版本的 Apache Shiro是無能爲力的Spring Security 來自 Spring Resource 社區,採用了註解的方式控制權限,熟悉Spring 的開發者很容易上手Spring Security。另外一個原因就是Spring Security易用與Spring boot工程,也容易集成到Spring Cloud構建的微服務系統中。
總結起來有以下幾個特點:
-
- 代碼耦合低
-
- 模塊化
-
- 控制粒度細
-
- 熟悉spring容易上手
-
- spring boot 或者spring cloud的集成簡單
-
- 龐大的社區與用戶
Spring Security 和Spring Boot Security的關係如下:
3. Security如何使用
3.1 創建
3.2 配置
3.3 security 配置
security需要自己寫一個配置類,配置類集成於WebSecurityConfigureAdapter
類
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private Environment environment;
@Value("${web.security.user.name}")
private String username;
@Value("${web.security.user.pswd}")
private String password;
@Value("${web.security.user.role}")
private String role;
@Value("${web.security.admin.name}")
private String adminName;
@Value("${web.security.admin.pswd}")
private String adminPswd;
@Value("${web.security.admin.role}")
private String adminRole;
@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication().withUser(username).password("{noop}" + password)
.roles(role.split(","));
auth.inMemoryAuthentication().withUser(adminName).password("{noop}" + adminPswd)
.roles(adminRole.split(","));
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/css/**", "/index").permitAll()
.antMatchers("/user/**").hasRole("USER")
.antMatchers("/admin/**").hasRole("ADMIN")
.and()
.formLogin().loginPage("/login").failureUrl("/login-error")
.and()
.exceptionHandling().accessDeniedPage("/401")
.and()
.logout().logoutSuccessUrl("/");
}
}
這裏的私有屬性是在config.properties裏面配置的用戶名、密碼與權限的信息。
這裏最好不要硬編碼。
3.3.1 configureGlobal方法
這個方法中,在內存中創建2個用戶的信息,用戶的用戶名、密碼以及密碼的加密方式,和其具有的角色。
密碼的加密方式:
這個方法裏只有短短的兩行代碼,但是其完成了非常多的操作:
-
- 應用的每一個請求都要認證
-
- 自動生成了一個登陸表單
-
- 用指定的用戶名密碼進行認證
-
- 用戶可以註銷
-
- 阻止了CSRF的攻擊
-
- Session Fixation的保護
-
- 安全Header
- HTTP Strict Transport Security for secure requests
- X-Content-Type_Options integration
- Cache Control
- X-XSS-Protection integration
- XFrake-Option integration to help prevent Clickjacking
-
- 集成了如下方法:
- HttpServletRequest#getRemoteUser()
- HttpServletRequest.html#getUserPrincipal()
- HttpServletRequest.html#isUserInRole(String)
- HttpServletRequest.html#login(String,String)
- HttpServletRequest.html#logout()
3.3.2 啓動登陸
其源碼如下
寫一個簡單的html界面用來標識登陸成功。
使用admin登陸
3.3.3 自定義配置 configure
代碼 | 配置內容 |
---|---|
“/css/**”,"/index" | 不需要認證即可訪問 |
“/user/**” | user目錄下的界面需要驗證user角色 |
“/admin” | admin目錄下的界面需要驗證admin角色 |
formLogin | 表單登陸界面是/login界面 |
failureUrl | 登陸失敗的地址是/login-error |
exceptionHandling | 異常會被重定向到401界面 |
logout | 支持註銷 |
logoutSuccessUrl | 註銷後重定向到/ |
3.3.4 controller
基於3.3.3的配置,實現controller
@Controller
public class MainController {
@RequestMapping("/")
public String root(){
return "redirect:/index";
}
@RequestMapping("/index")
public String index(){
return "index";
}
@RequestMapping("/user/index")
public String userIndex(){
return "user/index";
}
@RequestMapping("/admin/index")
public String adminIndex(){
return "admin/index";
}
@RequestMapping("/login")
public String login(){
return "login";
}
@RequestMapping("/login-error")
public String loginError(Model model){
model.addAttribute("loginError",true);
return "login";
}
@GetMapping("/401")
public String accessDenied(){
return "401";
}
}
3.3.5 界面
login.html
<!DOCTYPE html>
<html lang="en" xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Login page</title>
<base href="/">
<link rel="stylesheet" href="/css/main.css" th:href="@{/css/main.css}" />
</head>
<body>
<h1>Login page</h1>
<p th:if="${loginError}" class="error">用戶名或者密碼錯誤!</p>
<form th:action="@{/login}" method="post">
<label for="username">用戶名</label>:
<input type="text" id="username" name="username" autofocus="autofocus"/><br/>
<label for="password">密 碼</label>:
<input type="password" id="password" name="password" autofocus="autofocus"/><br/>
<input type="submit" value="登錄"/>
</form>
<p><a th:href="@{/index}">返回首頁</a> </p>
</body>
</html>
index.html
<!DOCTYPE html>
<html lang="en" xmlns="http://www.w3.org/1999/xhtml"
xmlns:th="http://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity5">
<head>
<meta charset="UTF-8">
<title>Hello Spring Boot Security for index</title>
<base href="/">
<link rel="stylesheet" href="css/main.css" th:href="@{/css/main.css}"/>
</head>
<body>
<h1>Hello Spring Boot Security for index</h1>
<p>這個界面沒有受到保護.</p>
<div th:fragment="logout" sec:authorize="isAuthenticated()">
登錄用戶:<span sec:authentication="name"/>
用戶角色:<span sec:authentication="principal.authorities"/>
<div>
<form action="#" th:action="@{/logout}" method="post">
<input type="submit" value="登出" />
</form>
</div>
</div>
<ul>
<li>點擊<a href="/user/index" th:href="@{/user/index}">去/user/index被保護的界面</a> </li>
<li>點擊<a href="/admin/index" th:href="@{/admin/index}">去/admin/index被保護的界面</a> </li>
</ul>
</body>
</html>
401.html
<!DOCTYPE html>
<html lang="en" xmlns="http://www.w3.org/1999/xhtml"
xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity4">
<head>
<meta charset="UTF-8">
<title>401 Page</title>
</head>
<body>
<div>
<div>
<h2>權限不夠</h2>
</div>
<div sec:authorize="isAuthenticated()">
<p>已有用戶登錄</p>
<p>用戶:<span sec:authentication="name" /></p>
<p>角色:<span sec:authentication="principal.authorities"/></p>
</div>
<div sec:authorize="isAnonymous()">
<p>未有用戶登錄</p>
</div>
<p>
拒絕訪問!
</p>
</div>
</body>
</html>
/user/index.html
<!DOCTYPE html>
<html lang="en" xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Hello Spring Security, User Index</title>
<base href="/">
<link rel="stylesheet" href="/css/main.css" th:href="@{/css/main.css}"/>
</head>
<body>
<div th:substituteby="index::logout"/>
<h1>這個界面是被保護界面,user角色可以訪問</h1>
<p><a href="/index" th:href="@{/index}">返回首頁</a> </p>
<p><a href="/admin" th:href="@{/admin/index}">去admin目錄下的index</a> </p>
</body>
</html>
/admin/index.html
<!DOCTYPE html>
<html lang="en" xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Hello Spring Security, Admin Index</title>
<base href="/">
<link rel="stylesheet" href="/css/main.css" th:href="@{/css/main.css}"/>
</head>
<body>
<div th:substituteby="index::logout"/>
<h1>這個界面是被保護界面,admin角色可以訪問</h1>
<p><a href="/index" th:href="@{/index}">返回首頁</a> </p>
<p><a href="/admin" th:href="@{/user/index}">去user目錄下的index</a> </p>
</body>
</html>
3.4 啓動
不登陸訪問/user或者admin的界面
訪問admin的界面登陸user用戶(user用戶只有user角色)
相反的,訪問user界面,登陸admin用戶(admin用戶有user和admin的角色)
訪問admin下的界面
然後登出
登陸user角色訪問user界面
登陸失敗
然後訪問admin的界面
4. security 方法保護
4.1 創建實體
public class Student {
private String name;
private int age;
private String like;
private Student(){
}
public static Student getBuild(){
return new Student();
}
public Student name(String name){
this.name = name;
return Student.this;
}
public Student age(int age){
this.age = age;
return Student.this;
}
public Student like(String like){
this.like = like;
return Student.this;
}
public String getName(){
return this.name;
}
public int getAge(){
return this.age;
}
public String getLike(){
return this.like;
}
}
4.2 創建服務
4.3 創建controller
4.4 訪問驗證
可以看到,我們在service上有兩個訪問,一個是獲取全部的學生的getStudentList的方法,這個方法只要有任意一個權限就能夠訪問。而另一個方法則必須擁有ADMIN的權限的用戶登錄才能進行訪問。
首先以USER權限進行登錄:
然後獲取所有的用戶
然後進行嘗試刪除學生–小美
發現其在controller接收到請求調用服務時,因權限不夠而發生異常,但是我們之前在配置時配置,當有異常出現時,自動重定向到401的界面。
所以,其展示的urlk地址是刪除的地址,但是界面的內容確是,401的內容。
接下來使用admin權限的用戶進行登錄,然後嘗試刪除學生。
這裏沒有任何返回值,表示已經刪除成功了,接下來重新獲取所有的學生:
5. 從數據庫中讀取用戶認證信息
5.1 創建
爲了防止因爲字符集的問題,需要手動增加依賴
5.2 配置
5.3 創建實體
@Entity
public class Subscriber implements UserDetails, Serializable {
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE)
private Long id;
@Column(nullable = false)
private String password;
@Column(nullable = false, unique = true)
private String username;
/*
* OneToMany 是一對多的關係,關係由多的記錄的屬性維護(一般情況)
* ManyToMany 是多對多的關係,關係由中間關係表維護(一般情況)
*/
@ManyToMany(cascade = CascadeType.ALL, fetch = FetchType.EAGER)
@JoinTable(name = "subscriber_role", joinColumns = @JoinColumn(name = "subscriber_id", referencedColumnName = "id"),
inverseJoinColumns = @JoinColumn(name = "role_id", referencedColumnName = "id"))
/*
* 這個JoinTable的大概含義是:
* 這個中間關係由關係表維護,表名是 user_role
* 關係表有兩個字段,一個是 user_id,其映射的值是user表的id
* 另一個是role_id,其映射的值是role表的id
*/
private List<Role> authorities;
public Subscriber(){
}
public Long getId(){
return id;
}
public void setId(Long id){
this.id = id;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return authorities;
}
public void setAuthorities(List<Role> authorities){
this.authorities = authorities;
}
@Override
public String getPassword() {
return password;
}
public void setPassword(String password){
this.password = password;
}
@Override
public String getUsername() {
return username;
}
public void setUsername(String username){
this.username = username;
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
5.4 dao
5.5 service
5.6 config
5.7 啓動驗證
這是啓動項目前的數據庫中所有的表
接着啓動
中間關係表與我們猜想的一致
不過有一點沒有想到,這個中間關係表竟然有外鍵。
用戶 | 權限 |
---|---|
userA | USER |
userB | USER |
userC | USER |
adminA | ADMIN |
adminB | ADMIN |
adminC | ADMIN |
allA | USER |
allA | ADMIN |
allB | USER |
allB | ADMIN |
allC | USER |
allC | ADMIN |
我們插入上述數據:
接下來用這些用戶嘗試登陸,並且結合4中的邏輯,進行驗證。
改動點如上圖所示
登陸
userA
adminA
allA
注意點:
這裏面有兩個坑:
1.password需要返回加密方式:
原因:
可選
2.role返回的時候需要加前綴
原因:
使用配置的時候會自動加這個前綴,現在使用jpa則不會自動加前綴
6. 總結
使用Spring Security 還是比較簡單的,沒有想象中那麼複雜。首先引入 Spring Security相關的依賴,然後寫一個配置類,該配置類繼承了 WebSecurityConfigurerAdapter,並在該配置類上加@EnableWebSecurity 註解開啓 Web Security。再需要配置 AuthenticationManagerBuilder,AuthenticationManagerBuilder 配置了讀取用戶的認證信息的方式,可以從內存中讀取,也可以
從數據庫中讀取,或者用其他的方式。其次,需要配置 HttpSecurity,HttpSecurity 配置了請求的認證規則,即哪些 URI 請求需要認證、哪些不需要,以及需要擁有什麼權限才能訪問。最後,如果需要開啓方法級別的安全配置,需要通過在配置類上加@EnableGlobalMethodSccuriy註解開啓,方法級別上的安全控制支持secureEnabled、jsr250Enabled和 prePostEnabled這3種
方式,用的最多的是prePostEnabled。其中,prePostEnabled 包括PreAuthorize和 PostAuthorize兩種形式,一般只用到PreAuthorize這種方式。