spring boot專案14:安全-基礎使用-MySQL(1)
JAVA 8
Spring Boot 2.5.3
MySQL 5.7.21(單機)
---
授人以漁:
1、Spring Boot Reference Documentation
This document is also available as Multi-page HTML, Single page HTML and PDF.
有PDF版本哦,下載下來!
有PDF版本哦(網頁版末尾的 /html5/ 改為 /pdf/),下載下來!
目錄
本文使用專案:
mysql-hello
Web專案,底層使用MySQL儲存資料,預設埠30000。
MySQL配置——後面會用到:
資料庫配置
# # MySQL on Ubuntu spring.datasource.url=jdbc:mysql://mylinux:3306/db_example?serverTimezone=Asia/Shanghai spring.datasource.username=springuser spring.datasource.password=ThePassword #spring.datasource.driver-class-name =com.mysql.jdbc.Driver # This is deprecated spring.datasource.driver-class-name =com.mysql.cj.jdbc.Driver spring.jpa.hibernate.ddl-auto=update spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL5InnoDBDialect # 開啟使用過程中執行的SQL語句 spring.jpa.show-sql: true
新增依賴包 spring-boot-starter-security:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
包結構:
啟動專案,此時,任何連結都不能訪問。
啟動日誌:
Using generated security password 後面是 預設使用者user的密碼。
在瀏覽器中訪問,彈出登入對話方塊:
登入頁-原始碼
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="description" content="">
<meta name="author" content="">
<title>Please sign in</title>
<link href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-/Y6pD6FV/Vv2HJnA6t+vslU6fwYXjCFtcEpHbNJ0lyAFsXTsjBbfaDjzALeQsN6M" crossorigin="anonymous">
<link href="https://getbootstrap.com/docs/4.0/examples/signin/signin.css" rel="stylesheet" crossorigin="anonymous"/>
</head>
<body>
<div class="container">
<form class="form-signin" method="post" action="/login">
<h2 class="form-signin-heading">Please sign in</h2>
<p>
<label for="username" class="sr-only">Username</label>
<input type="text" id="username" name="username" class="form-control" placeholder="Username" required autofocus>
</p>
<p>
<label for="password" class="sr-only">Password</label>
<input type="password" id="password" name="password" class="form-control" placeholder="Password" required>
</p>
<input name="_csrf" type="hidden" value="ed3f49ac-647f-4a59-b2e3-b24498725774" />
<button class="btn btn-lg btn-primary btn-block" type="submit">Sign in</button>
</form>
</div>
</body></html>
原始碼裡面有一個提交資料 /login 的表單——實現登入。
輸入 user、日誌中的密碼,登入成功。
除了上面的 /login 實現登入,還有一個 /logout 端點實現 退出登入:
隨機密碼,而且存在日誌裡面,不好。配置下面的可以實現固定使用者及密碼:
# 安全
spring.security.user.name=lib
spring.security.user.password=123
再次啟動,日誌沒有密碼資訊了。
瀏覽器登入,使用上面配置的 lib、123即可。
小結,
上面的專案很簡單,但有一定實用性了。
登入頁:login.html
static/login.html
<html>
<head>
<title>login:mysql-hello</title>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<style>
body {
background: #ddd;
}
</style>
</head>
<body>
<div>請登入:</div>
<form action="login.html" method="post">
<div>使用者名稱:<input type="text" name="username" placeholder="使用者名稱" /></div>
<div>密碼:<input type="password" name="password" placeholder="密碼" /></div>
<div><a href="#">忘記密碼?</a></div>
<div><input type="submit" value="登入" /> </div>
</form>
<br />
<br />
<div><a href="#">新使用者註冊</a></div>
</body>
</html>
注,包含username, password的<input>,注意<form>的action和method。來自部落格園
新增 AppWebSecurityConfig.java,繼承 WebSecurityConfigurerAdapter 並重寫 configure(HttpSecurity http):
@EnableWebSecurity
public class AppWebSecurityConfig extends WebSecurityConfigurerAdapter {
/**
* 自定義登入頁:login.html
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.anyRequest().authenticated()
.and()
.formLogin()
// 自定義登入頁
.loginPage("/login.html")
.permitAll()
.and()
.csrf().disable();
}
}
登入頁面:
輸入前面配置檔案中的使用者名稱、密碼,登入成功(首頁沒有建,顯示status=404),但可以測試其它連結的。
指定處理登入的URL-未通過
在formLogin()下,指定處理登入的URL:
.formLogin()
// 自定義登入頁
.loginPage("/login.html")
// 處理登入請求的URL
.loginProcessingUrl("/login")
但是,測試失敗,登入未成功。
錯誤資訊
瀏覽器頁面:
Whitelabel Error Page
This application has no explicit mapping for /error, so you are seeing this as a fallback.
Sat Sep 04 23:03:14 CST 2021
There was an unexpected error (type=Method Not Allowed, status=405).
---
應用日誌:
Resolved [org.springframework.web.HttpRequestMethodNotSupportedException: Request method 'POST' not supported]
Completed 405 METHOD_NOT_ALLOWED
"ERROR" dispatch for POST "/error", parameters={masked}
疑問:
為什麼呢?預設登入頁的 action不就是 “/login” 嗎?怎麼這裡配置了就不行呢?
像上面配置後,預設的/login 無效了?需要自己寫?怎麼寫?格式呢?TODO
登入返回值
上面的試驗中,登入成功後,跳轉到首頁。在真實的前後端分離系統中,登入後一般返回 成功與否的資訊,比如,一段JSON資料,再由前端決定怎麼處理——跳轉到哪裡。
在formLogin()下,配置 successHandler、failureHandler 分別實現登入成功、失敗後的邏輯。來自部落格園
.formLogin()
// 自定義登入頁
.loginPage("/login.html")
// 處理登入請求的URL
// 指定後登入失敗,註釋掉,TODO
// .loginProcessingUrl("/login")
// 登入成功的處理
.successHandler(new AuthenticationSuccessHandler() {
@Override
public void onAuthenticationSuccess(HttpServletRequest req, HttpServletResponse resp,
Authentication auth) throws IOException, ServletException {
resp.setContentType("application/json;charset=utf-8");
PrintWriter out = resp.getWriter();
out.write(ResultVO.getSuccess("登入成功").toString());
}
})
// 登入失敗的處理
.failureHandler(new AuthenticationFailureHandler() {
@Override
public void onAuthenticationFailure(HttpServletRequest req, HttpServletResponse resp,
AuthenticationException ex) throws IOException, ServletException {
resp.setContentType("application/json;charset=utf-8");
resp.setStatus(HttpStatus.UNAUTHORIZED.value());
PrintWriter out = resp.getWriter();
out.write(ResultVO.getFailed(HttpStatus.UNAUTHORIZED.value(), "登入失敗", "請重新登入").toString());
}
})
.permitAll()
.and()
注,ResultVO 是專案的一個 統一返回物件類,getSuccess、getFailed是其中的靜態方法。
測試結果:成功
前面的章節,只有一個使用者。本章介紹多個使用者的使用。
自定義一個 UserDetailsService Bean即可。
介面有很多實現類,其中:來自部落格園
1)InMemoryUserDetailsManager 的使用者資料 儲存到 記憶體,重啟後丟失
2)JdbcUserDetailsManager 的使用者資料 儲存到 資料庫,比如,MySQL資料庫
準備3個介面:
/security/admin/hello 需要ADMIN角色的使用者才可以訪問
/security/user/hello 需要USER角色的使用者才可以訪問
/security/app/hello 任意登入使用者都可以訪問
SecurityAdminController.java
@RestController
@RequestMapping(value="/security/admin")
@Slf4j
public class SecurityAdminController {
@GetMapping(value="/hello")
public String hello() {
return "hello, Admin";
}
}
其它兩個Controller類似。來自部落格園
更改 AppWebSecurityConfig:
之前的configure函式做了改動;
增加了 UserDetailsService Bean的生成函式,並增加了2個使用者對應不同的角色;
passwordEncoder函式 在 本文使用的 S.B.版本是必須的,否則發生異常,,但這個NoOpPasswordEncoder過期了,,原因及解決方案有待進一步研究,TODO
/**
* 試驗2:資源授權
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
// 使用角色
.antMatchers("/security/admin/**").hasRole("ADMIN")
.antMatchers("/security/user/**").hasRole("USER")
.antMatchers("/security/app/**").permitAll()
.anyRequest().authenticated()
.and()
.formLogin().permitAll()
.and()
.csrf().disable();
}
/**
* 基於記憶體資料庫的使用者資訊
*/
@Bean
public UserDetailsService userDetailsService() {
// 基於記憶體的使用者資訊:2個使用者,不同角色
InMemoryUserDetailsManager man = new InMemoryUserDetailsManager();
man.createUser(User.withUsername("user").password("123").roles("USER").build());
man.createUser(User.withUsername("admin").password("123").roles("ADMIN").build());
return man;
}
/**
* 必須有,否則發生異常
* 是否可以使用其它 PasswordEncoder 的實現類呢?
* 據說是 5.X版本之後預設啟用了 委派密碼編碼器 導致
* @author ben
* @date 2021-09-05 00:10:49 CST
* @return
*/
@Bean
public PasswordEncoder passwordEncoder() {
// 過時了?怎麼弄?TODO
// 因為不安全,只能用於測試、明文密碼驗證等,故廢棄
return NoOpPasswordEncoder.getInstance();
}
注意,上面的配置後,配置檔案中的 lib 使用者就不能使用了。
啟動應用,測試:
user、admin分別訪問前面的 3個介面。
使用者/介面 | user | admin |
/security/admin/hello | type=Forbidden, status=403 | hello, Admin |
/security/user/hello | hello, User | type=Forbidden, status=403 |
/security/app/hello | hello, APP | hello, APP |
符合預期。來自部落格園
更進一步:
動態管理使用者(增刪改查),或可以使用 容器中的 userDetailsService Bean——即上面配置生成了。
轉換為 InMemoryUserDetailsManager 後進行操作。
不過,應用重啟後,這些使用者資料丟失,意義不大,但從介面來看是可以做到的。
引入:來自部落格園
<!-- 使用JdbcUserDetailsManager時引入,沒有JPA的嗎? -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
注,本專案中,mysql-connector-java早已引入。
在MySQL建立資料表:找到 JdbcUserDetailsManager 類 對應的jar包(spring-security-core),DDL檔案位於 同一個jar包的 org.springframework.security.core.userdetails.jdbc.users.ddl 下
拷貝其中的語句,改其中的 varchar_ignorecase 為 varchar型別——MySQL支援。來自部落格園
使用改造後的語句到MySQL終端去執行:下圖展示執行成功,建立了兩張表 users、authorities
改造 AppWebSecurityConfig 的userDetailsService函式:
/**
* 使用JdbcUserDetailsManager
* 本應用的底層為 MySQL資料庫——上面的dataSource
*/
@Bean
public UserDetailsService userDetailsService() {
JdbcUserDetailsManager man = new JdbcUserDetailsManager();
System.out.println("dataSource=" + dataSource);
man.setDataSource(dataSource);
man.createUser(User.withUsername("user").password("123").roles("USER").build());
man.createUser(User.withUsername("admin").password("123").roles("ADMIN").build());
return man;
}
測試 兩個使用者對前面3個介面的許可權:測試成功,符合預期。
注,上面的 dataSource是 HikariPool-1:
JpaBaseConfiguration$JpaWebConfiguration : spring.jpa.open-in-view is enabled by default.
Therefore, database queries may be performed during view rendering. Explicitly configure
spring.jpa.open-in-view to disable this warning
dataSource=HikariDataSource (HikariPool-1)
啟動後,新建資料表的資料:
注意,角色建立時是 user、admin,但在 資料庫裡面是 以“ROLE_”開頭。
再次啟動應用,發生異常,啟動失敗,因為 user、admin在資料庫中已經存在了。來自部落格園
改造userDetailsService()函式:多了使用者存在性判斷
@Bean
public UserDetailsService userDetailsService() {
JdbcUserDetailsManager man = new JdbcUserDetailsManager();
man.setDataSource(dataSource);
if (!man.userExists("user")) {
man.createUser(User.withUsername("user").password("123").roles("USER").build());
}
if (!man.userExists("admin")) {
man.createUser(User.withUsername("admin").password("123").roles("ADMIN").build());
}
return man;
}
預設的資料庫模型肯定無法滿足生產的需求,比如,裡面的密碼都沒有加密。
Spring Security具有優良的擴充套件性,可以很好地實現自定義的資料庫模型。
---210905 01:55---寫到這兒了---
在使用JdbcUserDetailsManager的預設資料庫模型時,使用者、許可權是分成兩張表的。來自部落格園
本章介紹 基於自定義資料庫模型的認證和授權。
兩個步驟:1)實現UserDetails——使用者詳情;2)實現UserDetailsService——使用者詳情服務(類似於前面的2個Manager);
cofigure函式保持不變。
AppUser類,使用者實體類,也實現了 UserDetails 介面。
AppUser.java
package org.lib.mysqlhello.security.self;
import java.util.Collection;
import java.util.Date;
import java.util.List;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Transient;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
/**
* 自定義使用者
* @author ben
* @date 2021-09-05 09:26:11 CST
*/
@Entity
@Data
@Slf4j
public class AppUser implements UserDetails {
private static final long serialVersionUID = 210905L;
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(columnDefinition = "VARCHAR(50) NOT NULL UNIQUE")
private String username;
@Column(columnDefinition = "VARCHAR(384) NOT NULL")
private String password;
/**
* 使用者角色
* 多個角色使用英文都好(,)隔開
*/
@Column(columnDefinition = "VARCHAR(500) NOT NULL")
private String roles;
/**
* 使用者是否啟用:預設啟用
*/
@Column(columnDefinition = "BIT(1) DEFAULT true")
private Boolean enabled;
/**
* 有效期時間戳
* 預設為0 永久有效
*/
@Column(columnDefinition = "BIGINT DEFAULT 0")
private Long expiration;
/**
* 建立時間
*/
@Column(insertable = false, columnDefinition = "DATETIME DEFAULT NOW()")
private Date createTime;
/**
* 更新時間
*/
@Column(insertable = false, updatable = false, columnDefinition = "DATETIME DEFAULT NOW() ON UPDATE NOW()")
private Date updateTime;
// ----實現UserDetails介面----
// set函式已使用 @Data 註解建立
@Transient
private List<GrantedAuthority> authorities;
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return authorities;
}
@Override
public boolean isAccountNonExpired() {
if (this.expiration <= 0) {
return true;
}
if (this.expiration >= System.currentTimeMillis()) {
return true;
}
log.warn("使用者過期:id={}, expiration={}", this.id, this.expiration);
return false;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return this.enabled;
}
// ----實現UserDetails介面----
}
啟動應用,資料表建好了:
插入兩條資料(使用者):
-- 和之前不同,admin有兩個角色哦
insert into app_user(username, password, roles) values("admin", "123", "ROLE_ADMIN,ROLE_USER");
insert into app_user(username, password, roles) values("user", "123", "ROLE_USER");
AppUserDetailsService類:實現了 UserDetailsService介面,並使用 @Service註解。來自部落格園
@Service
public class AppUserDetailsService implements UserDetailsService {
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
return null;
}
}
上面的 AppUserDetailsService Bean 還無法使用:
前一章 的 userDetailsService() 函式也生成了 userDetailsService Bean,此時,雖然應用可以啟動,但是,無法登入——因為有兩個 userDetailsService Beans吧。
註釋掉AppWebSecurityConfig類 的 userDetailsService() 函式。來自部落格園
啟動應用,登入:AppUserDetailsService 還沒寫完導致
繼續改造 AppUserDetailsService...
改造後的 AppUserDetailsService:來自部落格園
package org.lib.mysqlhello.security.self;
import java.util.Objects;
import java.util.function.Consumer;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.authority.AuthorityUtils;
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;
/**
* AppUserDetailsService
* @author ben
* @date 2021-09-05 10:34:23 CST
*/
@Service
public class AppUserDetailsService implements UserDetailsService {
@Autowired
private AppUserDAO appUserDao;
private Consumer<Object> cs = System.out::println;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
AppUser user = appUserDao.findByUsername(username);
cs.accept("user 1=" + user);
if (Objects.isNull(user)) {
throw new UsernameNotFoundException("使用者不存在");
}
// 許可權集
// 使用Spring Security的AuthorityUtils:預設支援 英文逗號分開的許可權集
user.setAuthorities(AuthorityUtils.commaSeparatedStringToAuthorityList(user.getRoles()));
cs.accept("user 1=" + user);
return user;
}
}
啟動應用,測試已新增的使用者admin、user訪問各個介面:成功,符合預期。來自部落格園
UsernameNotFoundException說明:
繼承了 AuthenticationException——其下有若干的異常。
在AppUserDetailsService#loadUserByUsername函式中丟擲使用者過期異常
失敗了。
看來不是這麼用的。來自部落格園
記得 AppUser 實現 UserDetails介面 時,有一個 isAccountNonExpired() 函式,或許,過期的判斷已經實現了。
設定user過期時間——30秒有效期:
-- 當前時間+30秒過期
-- 注意使用 (unix_timestamp(now())+30)*1000!
-- 最開始只使用 now() 時驗證失敗/sad
mysql> update app_user set expiration=(unix_timestamp(now())+30)*1000 where id = 2;
在執行上面的語句後,啟動應用,使用 user登入:登入成功。
30秒後繼續操作,可以繼續操作,沒有被阻止。TODO
30秒後,在另一個瀏覽器重新登入:登入失敗,提示賬號過期。
回到之前已登入的瀏覽器操作:可以繼續,但會輸出 isAccountNonExpired() 函式的 過期日誌:來自部落格園
可是,怎麼阻止過期使用者繼續操作啊?!
》》》全文完《《《來自部落格園
補充:
public interface UserDetails extends Serializable
其下的User類
public class User implements UserDetails, CredentialsContainer {
public interface UserDetailsService
後記:
密碼沒有加密啊?
阻止過期使用者繼續訪問啊?來自部落格園
記住使用者?記住使用者多長時間?
登入過程中都做了什麼?過濾器、攔截器啥的?
自動登入呢?
基於token的登入呢?
……
看來,還要搞更多試驗、更多學習才是啊!
後面再寫一篇好了。來自部落格園
1、《Spring Security實戰》
書,作者:陳木鑫,2019年8月第1版
非常感謝。
2、