SpringSecurity
- 一.SpringSecurity簡介
- 二.SpringSecurity快速入門
-
- 1.入門項目需求
- 2.項目環境搭建
- 3.具體功能實作
-
- HttpBasic模式登入認證
- FormLogin模式登入認證
- 自定義登入驗證結果處理
- SpringSecurity的session管理及安全配置
-
- session建立時機
- 會話逾時配置
- session保護
- 同賬号多端登入被下線需求
- 三.RBAC權限管理模型
-
- 1.什麼是RBAC權限管理模型
- 2.RBAC權限管理系統
-
- 資料庫設計
- 從資料庫中動态加載使用者權限
- 動态加載資源鑒權規則
- 退出功能實作
- 記住我功能實作
一.SpringSecurity簡介
Spring Security 是強大的,且容易定制的,基于Spring開發的實作認證登入與資源授權的應用安全架構
官網:https://projects.spring.io/spring-security/
SpringSecurity 的核心功能:
- Authentication:認證,使用者登陸的驗證(解決你是誰的問題)
- Authorization:授權,授權系統資源的通路權限(解決你能幹什麼的問題)
- 安全防護,防止跨站請求,session 攻擊等
SpringSecurity與Shiro對比:
- Shiro架構更加輕量級,入門更加容易,使用起來也更加容易,不跟任何的架構或者容器捆綁,可以獨立運作
- SpringSecurity之是以看上去比shiro更複雜,其實是因為它引入了一些不常用的概念與規則。大家應該都知道2/8法則,這在SpringSecurity裡面展現的特别明顯,如果你隻學SpringSecurity最重要的那20%,這20%的複雜度和shiro基本是一緻的。也就是說,不重要的那80%,恰恰是SpringSecurity比shiro的“複雜度”。也就是說,如果有人能幫你把SpringSecurity最重要的那20%摘出來,二者的入門門檻、複雜度其實是差不太多的
- SpringSecurity預設含有對OAuth2.0的支援,與Spring Social一起使用完成社交媒體登入也比較友善。shiro在這方面隻能靠自己寫代碼實作
二.SpringSecurity快速入門
1.入門項目需求
由系統登入頁面,進行登入驗證到首頁,管理者admin使用者有日志管理,使用者管理,業務一,業務二的權限操作,普通user使用者隻有業務一,業務二的權限操作
2.項目環境搭建
pom.xml:
<dependencies>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.3</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
application.yml:
spring:
datasource:
username: root
password: 123456
url: jdbc:mysql:///sys_base?characterEncoding=UTF-8&serverTimezone=UTC
driver-class-name: com.mysql.cj.jdbc.Driver
#控制台列印sql語句
logging:
level:
com.baidu.springboot_10_simpledemo.mapper: debug
準備這6個簡單的頁面:不需要太好看,能區分開來就行,主要體會其中的邏輯
UserController:
@Controller
public class UserController {
@RequestMapping({"/","/index"})
public String index(String username,String password){
return "index";
}
@PostMapping("/login")
public String login(String username,String password){
return "index";
}
@RequestMapping("/user")
public String user(){
return "user";
}
@RequestMapping("/log")
public String logger(){
return "logger";
}
@RequestMapping("/service1")
public String service1(){
return "service1";
}
@RequestMapping("/service2")
public String service2(){
return "service2";
}
}
3.具體功能實作
HttpBasic模式登入認證
HttpBasic登入驗證模式是Spring Security實作登入驗證最簡單的一種方式,也可以說是最簡陋的一種方式。它的目的并不是保障登入驗證的絕對安全,而是提供一種“防君子不防小人”的登入驗證。
就好像是我小時候寫日記,都買一個帶小鎖頭的日記本,實際上這個小鎖頭有什麼用呢?如果真正想看的人用一根釘子都能撬開。它的作用就是:某天你的父母想偷看你的日記,拿出來一看還帶把鎖,那就算了吧,怪麻煩的。
舉一個我使用HttpBasic模式的進行登入驗證的例子:我曾經在一個公司擔任部門經理期間,開發了一套用于統計效率、分享知識、生成代碼、導出報表的Http接口。純粹是為了工作中提高效率,同時我又有一點點小私心,畢竟各部之間是有競争的,是以我給這套接口加上了HttpBasic驗證。公司裡随便一個技術人員,最多隻要給上一兩個小時,就可以把這個驗證破解了。說白了,這個工具的資料不那麼重要,加一道鎖的目的就是不讓它成為公開資料。如果有心人破解了,真想看看這裡面的資料,其實也無妨。這就是HttpBasic模式的典型應用場景。
config/SecurityConfig:
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.httpBasic()
.and()
.authorizeRequests().anyRequest()
.authenticated();
}
}
這段代碼的意思是,當任意一個請求發來,必須通過認證才可以通路系統權限
啟動項目時,背景會列印登入驗證密碼
當你浏覽器通路任意路徑時,就會由登入驗證
使用者名預設user
密碼是背景列印的
當然我們也可以通過application.yml指定配置使用者名密碼
spring:
security:
user:
name: admin
password: admin
FormLogin模式登入認證
Spring Security的HttpBasic模式,該模式比較簡單,隻是進行了通過攜帶Http的Header進行簡單的登入驗證,而且沒有定制的登入頁面,是以使用場景比較窄。
對于一個完整的應用系統,與登入驗證相關的頁面都是高度定制化的,非常美觀而且提供多種登入方式。這就需要Spring Security支援我們自己定制登入頁面,也就是接下來給大家介紹的formLogin模式登入認證模式。
formLogin模式的三要素:
- 登入認證邏輯
- 資源通路控制規則,如:資源權限、角色權限
- 使用者角色權限
一般來說,使用權限認證架構的的業務系統登入驗證邏輯是固定的,而資源通路控制規則和使用者資訊是從資料庫或其他存儲媒體靈活加載的。但這裡的所有的使用者、資源、權限資訊都是代碼配置寫死的,旨在為大家介紹formLogin認證模式,如何從資料庫加載權限認證相關資訊我會結合RBAC權限模型來給大家講解。
首先,和httpBasic一樣,我們要繼承WebSecurityConfigurerAdapter ,重寫configure(HttpSecurity http) 方法,該方法用來配置 登入驗證邏輯
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable() //禁用跨站csrf攻擊防禦
.formLogin()
.loginPage("/login.html")//使用者未登入時,通路任何資源都轉跳到該路徑,即登入頁面
.loginProcessingUrl("/login")//登入表單form中action的位址,也就是處理認證請求的路徑
.usernameParameter("username")///登入表單form中使用者名輸入框input的name名,不修改的話預設是username
.passwordParameter("password")//form中密碼輸入框input的name名,不修改的話預設是password
.defaultSuccessUrl("/index")//登入認證成功後預設轉跳的路徑
.and()
.authorizeRequests()
.antMatchers("/login.html","/login").permitAll()//不需要通過登入驗證就可以被通路的資源路徑
.antMatchers("/service1","/service2") //需要對外暴露的資源路徑
.hasAnyAuthority("ROLE_user","ROLE_admin") //user角色和admin角色都可以通路
.antMatchers("/log","/user")
.hasAnyRole("admin") //admin角色可以通路
.anyRequest().authenticated();
}
}
解釋代碼(結合代碼中的注釋了解)
- csrf().disable():CSRF(Cross-site request forgery跨站請求僞造,也被稱為“One Click Attack”或者Session Riding,通常縮寫為CSRF或者XSRF,是一種對網站的惡意利用。為了防止跨站送出攻擊,通常會配置csrf。Spring Security 3預設關閉csrf,Spring Security 4預設啟動了csrf。項目中啟用了 security,post請求無法通過,如果不采用csrf,可禁用security的csrf
- authorizeRequests():定制請求的授權規則
- antMatchers寫資源路徑,permitAll就是不需要通過登入驗證就可以被通路的資源路徑,其他的資源可以配置授權規則
- hasAuthority:擁有某個權限,如
,可以看做是對應資源權限配置對應的類似id
.antMatchers("/log").hasAuthority("log")
- hasAnyAuthority:擁有多個角色權限如ROLE_user、ROLE_admin
- hasRole:擁有角色,如admin
- hasAnyRole:擁有多個角色
- anyRequest().authenticated():表示所有請求必須先認證
在上文中,我們配置了登入驗證及資源通路的權限規則,我們還沒有具體的使用者,下面我們就來配置具體的使用者。重寫WebSecurityConfigurerAdapter 的 configure(AuthenticationManagerBuilder auth) 方法
@Override
public void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication()
.withUser("user")
.password(passwordEncoder().encode("123456"))
.roles("user")
.and()
.withUser("admin")
.password(passwordEncoder().encode("123456"))
//.authorities("log")
.roles("admin")
.and()
.passwordEncoder(passwordEncoder());//配置BCrypt加密
}
//配置 BCrypt 加密
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
- inMemoryAuthentication:在記憶體裡面存儲使用者的身份認證和授權資訊,真實開發中使用者資訊是從資料庫加載的,後面會講解
- withUser(“user”):使用者名是user
- password(passwordEncoder().encode(“123456”)):密碼是加密之後的123456
- roles(“user”):用于指定使用者的角色,一個使用者可以有多個角色
- authorities(“log”):指的是admin使用者擁有資源ID對應的資源通路的的權限(log),如果前面授權規則中使用
,那麼就對應用authorities(“log”)來配置
.antMatchers("/log").hasAuthority("log")
- 多個使用者用and()連接配接
在我們的實際開發中,登入頁面login.html和控制層Controller登入驗證’/login’都必須無條件的開放。除此之外,一些靜态資源如css、js檔案通常也都不需要驗證權限,我們需要将它們的通路權限也開放出來。下面就是實作的方法:重寫WebSecurityConfigurerAdapter類的configure(WebSecurity web) 方法
@Override
public void configure(WebSecurity web) {
//将項目中靜态資源路徑開放出來
web.ignoring().antMatchers( "/css/**", "/fonts/**", "/img/**", "/js/**");
}
測試效果:
可以看到,admin使用者可以四個頁面都可以通路,而user使用者隻能通路具體業務一、二,通路日志、使用者管理時,會報403的禁止通路的錯誤,後面會結合RBAC權限模型講解,如何在資料庫中擷取使用者資訊,權限資訊以及退出和記住我的功能
自定義登入驗證結果處理
自定義登入驗證成功結果處理:
需要自定義一個類MySuccessHandler繼承SavedRequestAwareAuthenticationSuccessHandler,并重寫
onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) 方法
@Component
public class MySuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {
@Value("${spring.security.loginType}")
private String loginType;
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws ServletException, IOException {
if (loginType.equalsIgnoreCase("json")){
//将傳回的對象轉換成json資料
ObjectMapper objectMapper = new ObjectMapper();
String json = objectMapper.writeValueAsString(Result.success(null));
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write(json);
}else{
//跳轉到登入之前請求的頁面
super.onAuthenticationSuccess(request, response, authentication);
}
}
}
- loginType:就是判斷條件,如果是json走的是我們自定義的邏輯,如果不是則走的父類的方法,跳轉到登入之前的請求頁面,loginType在application.yml中配置即可:
spring.security.loginType: JSON
- 我們自定義的邏輯,就是給頁面傳回一個Result成功的json資料,然後前端根據json資料進一步處理登入邏輯
然後在SecurityConfig中将自定義的MySuccessHandler 配置上就可以
//先注入
@Autowired
private MySuccessHandler mySuccessHandler;
http.csrf().disable()
.formLogin()
.loginPage("/login.html")
.usernameParameter("username")
.passwordParameter("password")
.loginProcessingUrl("/login")
//.defaultSuccessUrl("/index")
.successHandler(mySuccessHandler)
successHandler和defaultSuccessUrl不能同時使用
自定義登入驗證失敗結果處理:
需要自定義一個類MyFailureHandler繼承SimpleUrlAuthenticationFailureHandler并重寫onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) 方法
@Component
public class MyFailureHandler extends SimpleUrlAuthenticationFailureHandler {
@Value("${spring.security.loginType}")
private String loginType;
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
if (loginType.equalsIgnoreCase("json")) {
//将傳回的對象轉換成json資料
ObjectMapper objectMapper = new ObjectMapper();
String json = objectMapper.writeValueAsString(Result.fail("使用者名或密碼錯誤!"));
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write(json);
}else {
//重新跳轉到登入頁面
super.onAuthenticationFailure(request, response, exception);
}
}
}
和自定義成功一樣的邏輯,走的是自己的自定義的方法,像前端傳回一個Result失敗的資訊,前端再處理登入邏輯,如果不是走的自己的方法,就會調用父類的方法,重新跳轉到登入頁面
然後在SecurityConfig中将自定義的MyFailureHandler 配置上就可以
@Autowired
private MyFailureHandler myFailureHandler;
http.csrf().disable()
.formLogin()
.loginPage("/login.html")
.usernameParameter("username")
.passwordParameter("password")
.loginProcessingUrl("/login")
.successHandler(mySuccessHandler)
.failureHandler(myFailureHandler)
前端處理登入邏輯:
前端發送一個ajax異步請求,根據傳回的json資料處理登入邏輯,成功則跳轉到首頁,失敗則提示錯誤資訊并跳轉登入頁面
$.post("/login",{"username":username,"password":password,"remember":rememberMe},function (data) {
if (data.isok){
//成功
location.href="/index" target="_blank" rel="external nofollow" ;
}else {
//失敗
alert(data.msg);
location.href="/login.html" target="_blank" rel="external nofollow"
}
})
測試結果:
SpringSecurity的session管理及安全配置
session建立時機
- always: 如果session不存在總是需要建立
- ifRequired(預設): SpringSecurity僅在需要時才建立session
- never: SpringSecurity将永遠不會主動建立session,但是如果session已經存在,他将使用該session
- stateless: SpringSecurity不會建立session或使用任何session。适合與接口型的無狀态應用,該方式節省資源
配置方式:
.and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED) //session建立規則,預設,有需要時建立session
會話逾時配置
配置方式:
application.yml:
server:
servlet:
session:
timeout: 1m #最少配置一分鐘
SecurityConfig:
.and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED) //session建立規則,預設,有需要時建立session
.invalidSessionUrl("/login.html")//session逾時後的頁面,逾時跳轉到登入頁面
session保護
- 預設情況下,SpringSecurity啟用了migrationSession保護方式。即對于同一個cookie的SESSIONID使用者,每次登入驗證将會建立一個新的HTTP會話,舊的HTTP會話将無效,并且舊會話的屬性将被複制
- 設定為“none”時,原始會話不會無效
- 設定為“newSession”後,将建立一個幹淨的會話,而不會複制舊會話中的任何屬性
配置方式:
同賬号多端登入被下線需求
描述: 限制最大登入使用者數,就是一個賬号在一個裝置已登入,然後在另一個裝置同時登入該賬号,可以配置之前的賬号強制下線
配置方式:
.maximumSessions(1) //配置一個賬号最大裝置登入
.maxSessionsPreventsLogin(false) //false:當賬号在一個裝置上登入,允許其他裝置登入,但是之前登入的被迫下線,true就是不允許其他裝置登入
.expiredSessionStrategy(new MyExpiredSessionStrategy());
自定義一個類MyExpiredSessionStrategy實作SessionInformationExpiredStrategy接口
public class MyExpiredSessionStrategy implements SessionInformationExpiredStrategy {
@Override
public void onExpiredSessionDetected(SessionInformationExpiredEvent event) throws IOException, ServletException {
event.getResponse().setContentType("application/json;charset=UTF-8");
event.getResponse().getWriter().write("你的賬号在其他裝置登入,目前裝置被迫下線,如非本人操作,請及時更改密碼");
}
}
測試效果:
現在谷歌浏覽器上登入admin使用者,然後在火狐浏覽器登入admin使用者,此時會發現谷歌的賬戶被強制下線了
三.RBAC權限管理模型
1.什麼是RBAC權限管理模型
Role-Based Access Control ----- 基于角色的通路控制
就是使用者通過角色與權限進行關聯。簡單地說,一個使用者擁有若幹角色,每一個角色擁有若幹權限。這樣,就構造成“使用者-角色-權限”的授權模型
- 使用者:系統接口及通路的操作者
- 權限:能夠通路某接口後者做某種操作的授權資格
- 角色:具有一類相同操作權限的使用者的總稱,可以了解為一定數量的權限的集合,權限的載體
2.RBAC權限管理系統
資料庫設計
主要有6張表,使用者表存放使用者資訊,角色表存放角色資訊,權限表存放權限資訊,使用者表和角色表的關聯表存放使用者對應的角色,角色表和權限表的關聯表存放角色對應的權限
儲存加密後的密碼:
在test測試類中列印一下由passwordEncoder加密後的密碼就行,然後複制到資料庫中
@Resource
private PasswordEncoder passwordEncoder;
@Test
void contextLoads() {
System.out.println(passwordEncoder.encode("123456"));
}
從資料庫中動态加載使用者權限
UserDetails與UserDetailsService接口
UserDetails(本質上是個實體類,Security會自動從裡面取值進行對比),UserDetails就是使用者資訊,即:使用者名、密碼、該使用者所具有的權限。
源碼中的UserDetails接口都有哪些方法:
public interface UserDetails extends Serializable {
//擷取使用者的權限集合
Collection<? extends GrantedAuthority> getAuthorities();
//擷取密碼
String getPassword();
//擷取使用者名
String getUsername();
//賬号是否沒過期
boolean isAccountNonExpired();
//賬号是否沒被鎖定
boolean isAccountNonLocked();
//密碼是否沒過期
boolean isCredentialsNonExpired();
//賬戶是否可用
boolean isEnabled();
}
我們把這些資訊提供給Spring Security,Spring Security就知道怎麼做登入驗證了,這也展現了Springboot的整體理念,配置大于編碼,根本不需要我們自己寫Controller實作登入驗證邏輯。
是以我們需要自定義一個類實作UserDetails接口
@Component
public class MyUserDetails implements UserDetails {
Collection<? extends GrantedAuthority> authorities; //使用者權限集合
String password; //密碼
String username; //使用者名
boolean accountNonExpired; //賬戶是否沒過期
boolean accountNonLocked; //是否沒被鎖定
boolean credentialsNonExpired; //密碼是否沒過期
boolean enabled; //賬号是否可用
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return authorities;
}
@Override
public String getPassword() {
return password;
}
@Override
public String getUsername() {
return username;
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return enabled;
}
public void setAuthorities(Collection<? extends GrantedAuthority> authorities) {
this.authorities = authorities;
}
public void setPassword(String password) {
this.password = password;
}
public void setUsername(String username) {
this.username = username;
}
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;
}
}
一個适應于UserDetails的java POJO類,所謂的 UserDetails接口實作就是一些get方法。get方法由Spring Security調用,我們通過set方法或構造函數為 Spring Security提供UserDetails資料(從資料庫查詢)
UserDetailsService接口有一個方法叫做loadUserByUsername,我們實作動态加載使用者、角色、權限資訊就是通過實作該方法。函數見名知義:通過使用者名加載使用者。該方法的傳回值就是UserDetails
實作UserDetailsService接口之前,需要先提供三個資料層的查詢方法,一是通過使用者名查詢使用者資訊,二是根據使用者名查詢使用者角色清單,三是通過角色清單查詢權限清單。
@Mapper
public interface MyUserDetailsServiceMapper {
//根據userID查詢使用者資訊
@Select("SELECT username,password,enabled\n" +
"FROM sys_user u\n" +
"WHERE u.username = #{username}")
MyUserDetails findByUserName(@Param("username") String username);
//根據userID查詢使用者角色清單
@Select("SELECT role_code\n" +
"FROM sys_role r\n" +
"LEFT JOIN sys_user_role ur ON r.id = ur.role_id\n" +
"LEFT JOIN sys_user u ON u.id = ur.user_id\n" +
"WHERE u.username = #{username}")
List<String> findRoleByUserName(@Param("username")String username);
//根據使用者角色查詢使用者權限
@Select({
"<script>",
"SELECT url " ,
"FROM sys_menu m " ,
"LEFT JOIN sys_role_menu rm ON m.id = rm.menu_id " ,
"LEFT JOIN sys_role r ON r.id = rm.role_id ",
"WHERE r.role_code IN ",
"<foreach collection='roleCodes' item='roleCode' open='(' separator=',' close=')'>",
"#{roleCode}",
"</foreach>",
"</script>"
})
List<String> findAuthorityByRoleCodes(@Param("roleCodes")List<String> roleCodes);
}
實作UserDetailsService接口,實作動态加載使用者、角色、權限資訊
@Component
public class MyUserDetailsService implements UserDetailsService {
@Resource
private MyUserDetailsServiceMapper mapper;
@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
//加載基礎使用者資訊
MyUserDetails myUserDetails = mapper.findByUserName(s);
//加載使用者角色清單
List<String> roleCodes = mapper.findRoleByUserName(s);
//通過使用者角色清單加載使用者資源權限清單
List<String> authorities = mapper.findAuthorityByRoleCodes(roleCodes);
//角色是一個特殊的權限,ROLE_字首
roleCodes = roleCodes.stream()
.map(rc -> "ROLE_" +rc) //每個對象前加字首
.collect(Collectors.toList()); //再轉換回List
authorities.addAll(roleCodes); //添加修改好字首的角色字首的角色權限
//把權限類型的權限給UserDetails
myUserDetails.setAuthorities(
//逗号分隔的字元串轉換成權限類型清單
AuthorityUtils.commaSeparatedStringToAuthorityList(
//List轉字元串,逗号分隔
String.join(",",authorities)
)
);
return myUserDetails;
}
}
在SecurityConfig類中注冊自定義的UserDetailsService,重寫WebSecurityConfigurerAdapter的 configure(AuthenticationManagerBuilder auth)方法
//先注入
@Autowired
private MyUserDetailsService myUserDetailsService;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(myUserDetailsService)
.passwordEncoder(passwordEncoder());
}
動态加載資源鑒權規則
- 首先通過登入使用者名加載使用者的urls(即資源通路路徑、資源唯一辨別)
- 如果urls清單中任何一個元素,能夠和request.getRequestURI()請求資源路徑相比對,則表示該使用者具有通路該資源的權限
- hasPermission有兩個參數,第一個參數是HttpServletRequest ,第二個參數是Authentication認證主體
- 使用者每一次通路系統資源的時候,都會執行這個方法,判斷該使用者是否具有通路該資源的權限
Mapper:
@Mapper
public interface MyRBACServiceMapper {
@Select("SELECT url \n" +
"FROM sys_menu m\n" +
"LEFT JOIN sys_role_menu rm ON m.id = rm.menu_id\n" +
"LEFT JOIN sys_role r ON r.id = rm.role_id\n" +
"LEFT JOIN sys_user_role ur ON r.id = ur.role_id\n" +
"LEFT JOIN sys_user u ON u.id = ur.user_id\n" +
"WHERE u.username = #{username}")
List<String> findUrlsByUserName(@Param("username") String username);
}
Service:
@Component("rbacService")
public class MyRBACService {
@Resource
private MyRBACServiceMapper mapper;
/**
*判斷使用者是否具有該請求資源的通路權限
*/
public boolean hasPermission(HttpServletRequest request, Authentication authentication){
//從security中拿出使用者主體,實際上是我們之前封裝的UserDetails,但是又被封了一層
Object principal = authentication.getPrincipal();
if (principal instanceof UserDetails){
String username = ((UserDetails) principal).getUsername();
List<String> urls = mapper.findUrlsByUserName(username);
for (String url : urls){
if (url.equals(request.getRequestURI())){
return true;
}
}
}
return false;
}
}
注冊rbacService
.and()
.authorizeRequests()
.antMatchers("/login.html","/login").permitAll()
.antMatchers("/index").authenticated()
.anyRequest().access("@rbacService.hasPermission(request,authentication)")
- 登入頁面“login.html”和登入認證處理路徑“/login”需完全對外開發,不需任何鑒權就可以通路
- 首頁 "/index"必須authenticated,即:登陸之後才能通路。不做其他額外鑒權規則控制
- 最後,其他的資源的通路我們通過權限規則表達式實作,表達式規則中使用了rbacService,這個類我們自定義實作。該類服務hasPermission從資料庫動态加載資源比對規則,進行資源通路鑒權
測試效果:
和之前靜态在記憶體中配置的效果一樣,不過所有使用者、角色、權限都是動态從資料庫擷取
大家退出功能等急了吧,馬上講解,哈哈哈
退出功能實作
退出功能相對簡單,不做詳細講解
配置方式:
SecurityConfig:
//開啟退出功能
http.logout().logoutSuccessUrl("/login.html"); //退出成功後來到的頁面(登入頁面)
前端退出按鈕:
<form action="/logout" method="post">
<input type="submit" value="登出"/>
</form>
記住我功能實作
SpringSecurity也提供了記住我的功能,這是一個很常見的功能,通常都是将使用者資訊儲存在cookie裡面,存在用戶端,達到記住我的功能
配置方式:
//開啟記住我功能
http.rememberMe()
.rememberMeParameter("remember") //傳入參數名稱
.rememberMeCookieName("remember-me-cookie") //cookie名稱
.tokenValiditySeconds(2*24*60*60); //cookie保留時間
前端:
登入後浏覽器的cookie:
可以檢測一下,先用浏覽器登入系統,然後直接關閉浏覽器,再次打開浏覽器請求,可直接通路,無需登入
這篇博文也算是我自己的一個學習筆記吧,有什麼說錯的地方,望大家指出,相信你如果認真看完,收獲一定頗豐!後續我還會出一些權限管理系統的實戰項目