👍SpringSecurity單體項目最佳實踐
- 2022 年 11 月 10 日
- 筆記
- springboot, SpringSecurity
SpringSecurity單體項目最佳實踐
到這裡,我們的SpringSecurity就已經完結啦,文章中可能有些地方不能做到全面覆蓋,視頻教程地址
1、搭建環境
-
建議下載初始項目,跟着文章一步一步搭建。加深對於
SpringSecurity
的理解。 -
❌ 需要將
application.properties
的數據庫配置,改成您自己對應的信息 -
❌ 如若依賴問題,修改Idea Maven,改成自己的
-
❌ 還需將Jdk版本改成您自己所使用的的版本。項目使用的是
JDK12
-
❌ 數據庫腳本在完成項目中的sql文件中
2、簡單使用
-
添加
SpringSecurity
依賴 -
❌ 註:這裡沒有申明版本號,是由於我們項目繼承的SpringBoot父項目,它已經為我們適配了對於的版本。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
然後啟動項目即可:
- 🅰️ 觀察控制台:這是
SpringSecurity
為我們臨時生成的密碼,默認用戶名為user
- 🅱️ 回到瀏覽器,輸入
//localhost:8080/community
,由於我們此時還未登陸,會重定向到默認創建的登陸頁面中,這是SpringSecurity默認為我們做的。 - 🕐 輸入控制台的密碼,即可進入到系統,
3、自定義使用
- 相信小夥伴們已經對
SpringSecurity
已經有了初步的了解,但是正常的項目中,不可能採用這個默認登陸頁面呀,這點SpringSecurity
也早就想到了。 - 當然可以自定義登陸頁面,但是在自定義登陸頁面之前,我們需要簡單處理一下我們的實體類。
在用戶登錄時,系統會根據用戶名,從存儲設備查找該用戶的密碼及權限等,將其組裝成一個
UserDetails
對象。並用UserDetails
中的數據對用戶進行認證,決定其輸入的用戶名/密碼是否正確。
- 🔻 觀察
UserDetails
結構
public interface UserDetails extends Serializable {
Collection<? extends GrantedAuthority> getAuthorities();//權限
String getPassword(); //密碼
String getUsername(); //用戶名
boolean isAccountNonExpired(); //賬號是否未過期
boolean isAccountNonLocked(); //賬號是否未鎖定
boolean isCredentialsNonExpired();//密碼是否未過期
boolean isEnabled(); //是否激活
- 裏面定義了許多關於用戶的信息,可以看到它是一個接口,並不能直接使用。那麼肯定就有默認的實現類,要不然我們上面的登陸功能是怎麼完成的呢。
- 🅿️ 此項目中採用 實體類繼承它的方式來完成。
@Data
public class User implements UserDetails {
private int id;
private String username;
private String password;
private String salt;
private String email;
private int type; // 1 管理員 2普通用戶
private int status;
private String activationCode;
private String headerUrl;
private Date createTime;
//權限
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
List<GrantedAuthority> permissions = new ArrayList<>();
permissions.add((GrantedAuthority) () -> {
switch (type) { //
case 1:
return "ADMIN";
default:
return "USER";
}
});
return permissions;
}
// true 帳戶未過期
@Override
public boolean isAccountNonExpired() {
return true;
}
// true 帳戶未鎖定
@Override
public boolean isAccountNonLocked() {
return true;
}
// true 憑證未過期
@Override
public boolean isCredentialsNonExpired() {
return true;
}
// true 賬號是否可用
@Override
public boolean isEnabled() {
return true;
}
}
- ⁉️ 定義了UserDetails之後,當然還遠遠不夠,哪個方法查詢數據庫來獲取我們的用戶信息呢?就是Security中的
UserDetailsService
接口
- ⭕️ 它肯定也有默認實現類的,但是我們需要查詢數據庫對應的用戶數據,所以我們還是採用
自定義
的方式去完成。
@Service
public class UserService implements UserDetailsService {
@Resource
private UserMapper userMapper;
// 根據用戶名去查詢用戶數據
public User findUserByName(String username) {
return userMapper.selectByName(username);
}
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
return this.findUserByName(username);
}
}
- ⛎ 完成到這裡,對於用戶信息的功能已經實現,但是我們還沒有配置我們的登陸界面。
配置Security
- ❗️ 在
config
目錄下創建SecurityConfig
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Resource
private UserService userService;
}
- ❗️ 正常項目中,肯定會有許多的靜態資源,這些都可以在不登錄的情況下訪問,如css、js等
@Override
public void configure(WebSecurity web) throws Exception {
// 忽略靜態資源
web.ignoring().antMatchers("/resources/**");
}
- ❗️ 當然我們上面的
UserService
只實現了認證的查詢,並沒有配置在何時去調用這個類。
認證規則二選其一即可
// AuthenticationManager: 認證的核心接口
// AuthenticationManagerBuilder: 用戶構建AuthenticationManager對象的工廠類
// ProviderManager: AuthenticationManager默認使用的實現類
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//內置的認證規則
//auth.userDetailsService(userService).passwordEncoder(new BCryptPasswordEncoder());
// 自定義認證規則
// AuthenticationProvider: ProviderManager持有一組AuthenticationProvider,每個AuthenticationProvider負責一種認證
// 委託模式:
// AuthenticationProvider: 就好比登陸方式,不僅有密碼登錄,且還有微信,等其他登陸方式,每一種登陸方式對應一個AuthenticationProvider
auth.authenticationProvider(new AuthenticationProvider() {
// Authentication: 用於封裝認證信息的接口,不同實現類代表不同類型的認證信息
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
String username = authentication.getName();
String password = authentication.getCredentials().toString();
User user = userService.findUserByName(username);
if (user == null) {
throw new UsernameNotFoundException("賬號或密碼錯誤!");
}
password = CommunityUtil.md5(password + user.getSalt());
if (!user.getPassword().equals(password)) {
throw new BadCredentialsException("賬號或密碼錯誤!");
}
// principal:認證的主要信息 credentials:代表用戶 authorities:權限信息
return new UsernamePasswordAuthenticationToken(user, user.getPassword(), user.getAuthorities());
}
// 當前的AuthenticationProvider 支持哪種類型的認證。
@Override
public boolean supports(Class<?> aClass) {
// UsernamePasswordAuthenticationToken: Authentication接口常用的實現類
// 這樣配置,我們當前項目只支持UsernamePasswordAuthenticationToken的認證
return UsernamePasswordAuthenticationToken.class.equals(aClass);
}
});
}
- ❗️ 配置了以上步驟,是不是覺得Security挺麻煩的,別急馬上到頭了。
@Override
protected void configure(HttpSecurity http) throws Exception {
// 登陸相關配置
http.formLogin()
.loginPage("/loginpage") // 登陸頁面
.loginProcessingUrl("/login") // 處理登陸請求的路徑
.successHandler(new AuthenticationSuccessHandler() { // 認證成功處理器
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
// 重定向到主頁面
response.sendRedirect(request.getContextPath() + "/index");
}
})
.failureHandler(new AuthenticationFailureHandler() { // 認證失敗處理器
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException {
// 請求轉發到登陸頁面
// 因為在項目中,登陸失敗後,往往需要攜帶錯誤信息到頁面展示,所有採用請求轉發的方式
request.setAttribute("error", e.getMessage());
request.getRequestDispatcher("/loginpage").forward(request, response);
}
});
// 退出相關配置
http.logout().logoutUrl("/logout").logoutSuccessUrl("/index"); // 退出後重定向到的接口
// 授權配置 配置什麼路徑只能什麼權限訪問
http.authorizeRequests()
.antMatchers("/letter").hasAnyAuthority("USER", "ADMIN")
.antMatchers("/admin").hasAnyAuthority("ADMIN")
.and().exceptionHandling().accessDeniedPage("/denied"); //無權限時,重回定向到的頁面
}
- 現在對於後端的配置就完成啦,前端界面建議直接從完成的項目中copy
- ❗️ 一定要檢查數據庫有沒有對應的用戶數據哦!!!
- 接下來就是你們的時間啦。自行測試
- 但是正常的項目中,登陸功能一定會有驗證碼的存在,SpringSecurity也想到了這一點,我們都知道SpringSecurity是由一大串過濾器來完成對應功能的,也就是說,我們需要在登陸校驗之前完成對於驗證碼的校驗。如下:
// 增加Filter 處理驗證碼
http.addFilterBefore(new Filter() {
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) servletRequest;
HttpServletResponse response = (HttpServletResponse) servletResponse;
if (request.getServletPath().equals("/login")) {
// 正常項目中 驗證碼會存儲在Session或者Redis中,為了方便。此項目的驗證碼都是1234
String verifyCode = request.getParameter("verifyCode");
if (!"1234".equals(verifyCode)) {
request.setAttribute("error", "驗證碼錯誤!");
request.getRequestDispatcher("/loginpage").forward(request, response);
return;
}
}
// 放行請求,執行到下一個過濾器
filterChain.doFilter(request, response);
}
}, UsernamePasswordAuthenticationFilter.class);
// 記住我功能
http.rememberMe()
.tokenRepository(new InMemoryTokenRepositoryImpl()) // 用戶的令牌存儲到哪,InMemoryTokenRepositoryImpl 存儲到內存中
.tokenValiditySeconds(3600 * 24) // 過期時間
.userDetailsService(userService);// 當關閉瀏覽器後,第二次訪問,去拿重新查詢用戶的數據
到這裡,我們的SpringSecurity就已經完結啦,文章中可能有些地方不能做到全面覆蓋,視頻教程地址