本篇作為SpringBoot2.1版本的個人開發框架 子章節,請先閱讀SpringBoot2.1版本的個人開發框架再次閱讀本篇文章
參考:
在上一篇文章我們對spring security有了初步認識以後,我們這篇主要實現 從資料庫查詢使用者來進行使用者是否具有登陸的認證。
資料庫表的設計
參考:
- RBAC許可權管理 這篇文章講的非常詳細,只不過有點久遠,12年我還在上高一。。。
我們在做使用者登陸之前,我們先要設計資料庫表,RBAC(Role-Based Access Control,基於角色的訪問控制),就是使用者通過角色與許可權進行關聯。簡單地說,一個使用者擁有若干角色,每一個角色擁有若干許可權。這樣,就構造成“使用者-角色-許可權”的授權模型。在這種模型中,使用者與角色之間,角色與許可權之間,一般者是多對多的關係。
按照我參考的部落格中的設計好了五張基礎表,欄位的話可以根據自己的需求變化,生成的sql檔案我放到我專案中了。表結構確定好以後,我們在專案中把系統使用者的entity、dao、service、以及dao對應的xml都設定好,這裡我就不一一展示程式碼了,可以拿之前的MybatisPlus的自動生成,正好自己又可以複習一遍之前的,新增好以後可以現在測試類中確認一下,沒有錯誤再進行下一步。
security核心類
在上一篇筆記中我們知道security的核心類之一的WebSecurityConfigurerAdapter,我們可以在這個配置類中建立自己的使用者,放行哪個請求,認證什麼請求,在這篇筆記我們要實現從資料庫中查詢使用者,所以我們要對以下核心類做實現。
- UserDetails介面
- UserDetailsService介面
UserDetails介面
UserDetails介面是security為我們提供的可擴充套件的使用者資訊介面,儲存一些非敏感類的資訊,在security模組下entity包中實現此介面類,其實就是一個實體類。
package com.ywh.security.entity;
import com.fasterxml.jackson.annotation.JsonIgnore;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.Collection;
import java.util.Collections;
/**
* CreateTime: 2019-01-24 16:10
* ClassName: SecurityUserDetails
* Package: com.ywh.security.entity
* Describe:
* security的使用者詳情類
*
* @author YWH
*/
public class SecurityUserDetails implements UserDetails {
/**
* 使用者的密碼
*/
private String password;
/**
* 使用者的名字
*/
private String username;
/**
* 使用者狀態,1 表示有效使用者, 0表示無效使用者
*/
private Integer state;
/**
* 使用者的許可權,可以把使用者的角色資訊的集合先放進來,角色代表著許可權
*/
private Collection<? extends GrantedAuthority> authorties;
public SecurityUserDetails(String password, String username, Integer state, Collection<? extends GrantedAuthority> authorties) {
this.password = password;
this.username = username;
this.state = state;
this.authorties = authorties;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return authorties;
}
@Override
public String getPassword() {
return password;
}
@Override
public String getUsername() {
return username;
}
/**
* 指示使用者的帳戶是否已過期。過期的帳戶無法通過身份驗證。
* @return true如果使用者的帳戶有效(即未過期), false如果不再有效(即已過期)
*/
@JsonIgnore
@Override
public boolean isAccountNonExpired() {
return true;
}
/**
* 指示使用者是鎖定還是解鎖。鎖定的使用者無法進行身份驗證。
* @return true是未鎖定,false是已鎖定
*/
@JsonIgnore
@Override
public boolean isAccountNonLocked() {
return true;
}
/**
* 指示使用者的憑據(密碼)是否已過期。過期的憑據會阻止身份驗證
* @return true如果使用者的憑證有效(即未過期), false如果不再有效(即已過期)
*/
@JsonIgnore
@Override
public boolean isCredentialsNonExpired() {
return true;
}
/**
* 指示使用者是啟用還是禁用。禁用的使用者無法進行身份驗證。
* @return true使用者已啟用,false使用者已經禁用
*/
@JsonIgnore
@Override
public boolean isEnabled() {
return state == 1;
}
}
複製程式碼
UserDetailsService介面
此介面主要重寫一個方法就好了loadUserByUsername,這個方法主要作用就是通過使用者名稱查詢使用者的詳細資訊,然後放到我們的UserDetails 實現的子類中,儲存在security的SecurityContextHolder中,上一篇我們通過這個來獲取使用者資訊的。
在security模組中的service/impl中實現此介面,selectByUserName方法就是一個普通的dao介面,在mapper.xml檔案中定義好sql語句,由於很簡單我就不貼了
package com.ywh.security.service.impl;
/**
* CreateTime: 2019-01-25 16:39
* ClassName: SecurityUserDetailsServiceImpl
* Package: com.ywh.security.service.impl
* Describe:
* UserDetailService的實現類
* 這個@Primary表示這個類所繼承的介面有多個實現類,當不知道引入哪個的時候,優先使用@Primary所註解的類
* @author YWH
*/
@Primary
@Service
public class SecurityUserDetailsServiceImpl implements UserDetailsService {
// 使用者查詢介面
private SysUserDao sysUserDao;
@Autowired
public SecurityUserDetailsServiceImpl(SysUserDao sysUserDao) {
this.sysUserDao = sysUserDao;
}
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
SysUserEntity sysUserEntity = sysUserDao.selectByUserName(username);
if(sysUserEntity != null){
// stream java8的新特性,Stream 使用一種類似用 SQL 語句從資料庫查詢資料的直觀方式來提供一種對 Java 集合運算和表達的高階抽象。
// 參考http://www.runoob.com/java/java8-streams.html
List<SimpleGrantedAuthority> collect = sysUserEntity.getRoles().stream().map(SysRoleEntity::getSysRoleName)
.map(SimpleGrantedAuthority::new).collect(Collectors.toList());
return new SecurityUserDetails(sysUserEntity.getSysUserPassword(),sysUserEntity.getSysUserName(),sysUserEntity.getSysUserState(),collect);
}
throw MyExceptionUtil.mxe(String.format("'%s'.這個使用者不存在", username));
}
}
複製程式碼
修改WebSecurityConfigurerAdapter實現類
在上一篇中我們對此類進行了重寫,我們知道使用者配置是重寫configure(AuthenticationManagerBuilder auth)方法,我們是通過記憶體管理器來建立使用者的,這回我們實現了UserDetailsService介面,我們就可以通過這個來查詢使用者並使用。 具體修改如下:
package com.ywh.security.config;
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfigurer extends WebSecurityConfigurerAdapter {
private UserDetailsService userDetailsService;
@Autowired
public SecurityConfigurer(UserDetailsService userDetailsService) {
this.userDetailsService = userDetailsService;
}
/**
* 使用者資訊配置
* @param auth 使用者資訊管理器
* @throws Exception 異常資訊
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//修改以前
// auth
// .inMemoryAuthentication()
// .withUser("root")
// .password("root")
// .roles("user")
// .and()
// .passwordEncoder(CharEncoder.getINSTANCE());
//修改以後
auth
.userDetailsService(this.userDetailsService)
.passwordEncoder(passwordEncoder());
}
/**
* 密碼加密
* @return 返回加密後的密碼
*/
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
複製程式碼
重啟專案後我們可以看到效果如下,ywh這個使用者是資料庫中的,這個使用者的密碼是我手動呼叫PasswordEncoder 加密過後放到資料庫中的,密碼都是123。
@Autowired
private PasswordEncoder passwordEncoder;
@Test
public void getOneTest(){
System.out.println("password:" + passwordEncoder.encode("123"));
}
複製程式碼
如果輸入一個資料庫中沒有的使用者,則會提示你查無此人
自定義登陸
到此步之後我們完成了從資料庫中查詢使用者登陸的操作,但是登陸頁面是security給我們預設的頁面。
想使用我們自定義的頁面,security繼續替我們認證,在core模組下的resource檔案下建立public目錄後建立我們自定義的login.html登陸頁面
<!DOCTYPE HTML>
<html>
<head>
<title>登陸頁面</title>
</head>
<body>
<h2>表單登入</h2>
<form action="/core/login" method="post">
<table>
<tr>
<td>使用者名稱:</td>
<td>
<input type="text" placeholder="使用者名稱" name="username" required="required" />
</td>
</tr>
<tr>
<td>密碼:</td>
<td>
<input type="password" placeholder="密碼" name="password" required="required" />
</td>
</tr>
<tr>
<td colspan="2">
<button type="submit">登入</button>
</td>
</tr>
</table>
</form>
</body>
</html>
複製程式碼
要注意form表單中的action屬性,這裡的值要與後面在security配置類中的loginProcessingUrl("/login")相同,我寫成/core/login是因為我配置了context-path,如果你沒配置不用寫/core字首
訪問的index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>測試</title>
</head>
<body>
登陸了!!!
</body>
</html>
複製程式碼
修改security配置類的configure(HttpSecurity httpSecurity)方法
/**
* 配置如何通過攔截器保護我們的請求,哪些能通過哪些不能通過,允許對特定的http請求基於安全考慮進行配置
* @param httpSecurity http
* @throws Exception 異常
*/
@Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
httpSecurity
// 暫時禁用csrc否則無法提交
.csrf().disable()
// 設定最多一個使用者登入,如果第二個使用者登陸則第一使用者被踢出,並跳轉到登陸頁面
.sessionManagement().maximumSessions(1).expiredUrl("/login.html");
httpSecurity
// 開始認證
.authorizeRequests()
// 對靜態檔案和登陸頁面放行
.antMatchers("/static/**").permitAll()
.antMatchers("/auth/**").permitAll()
.antMatchers("/login.html").permitAll()
// 其他請求需要認證登陸
.anyRequest().authenticated();
httpSecurity
// 表單登陸
.formLogin()
// 設定跳轉的登陸頁面
.loginPage("/login.html")
// .failureUrl("/auth/login?error") 設定如果出錯跳轉到哪個頁面
// security預設使用的就是login路徑認證,如果想使用自定義自行修改就可以了
.loginProcessingUrl("/login")
// 如果直接訪問登入頁面,則登入成功後重定向到這個頁面,否則跳轉到之前想要訪問的頁面
.defaultSuccessUrl("/index.html");
}
複製程式碼
loginPage()中也可以填寫介面路徑,通過介面來返回登陸頁面;通過以上程式碼實現了我們自己定義的登陸頁面,security會為我們攔截並認證的,只是建立了兩個頁面和增加了兩個屬性。
想要自定義實現登陸成功的邏輯,可以配置successHandler()方法,要實現的介面為AuthenticationSuccessHandler,重寫其中的方法為onAuthenticationSuccess()。
自定義退出
想要實現自定義退出功能的邏輯,需要實現AuthenticationFailureHandler 介面,重寫其中的onAuthenticationFailure方法。或者在配置中的方法中寫匿名內部類。
而關於security中的退出,也有給我們預設實現,通過“/logout”就可以實現退出功能了,在index.html介面新增一個 a 標籤即可。
<a href="/core/logout">退出</a>
複製程式碼
再次提示我這裡寫/core是因為我配置了context-path,如果你沒有配置直接寫/logout即可,預設security機制會進行如下操作:
- 使HttpSession無效
- 清理記住密碼
- 清理SecurityContextHolder
- 重定向/login?logout
當然我們也可以自定義退出,在configure(HttpSecurity httpSecurity)方法中新增以下內容,這裡我只實現了最簡單的操作
httpSecurity
// 登出
.logout()
// 登出處理,使用security預設的logout,也可以自定義路徑,實現即可
.logoutUrl("/logout")
// 登出成功後跳轉到哪個頁面
.logoutSuccessUrl("/login.html")
.logoutSuccessHandler((request, response, authentication) -> {
//登出成功處理函式
System.out.println("logout success");
response.sendRedirect("/core/login.html");
})
.addLogoutHandler((request, response, authentication) ->{
//登出處理函式
System.out.println("logout------");
})
// 清理Session
.invalidateHttpSession(true);
複製程式碼
關於security上面我們實現了最基本的功能,它還可以做更多的事情,比如記住我的密碼,第三方登入等等。