流量如水,文章就是一個瓶子,標題是瓶口,內容是瓶身。 瓶子裝水有多快依賴瓶口大小,瓶子裝水有多少依賴瓶身大小。
Spring Security 淺析
Spring Security 是什麼?
Spring Security 是 Spring 家族中一個安全管理框架,實際上,在 Spring Boot 出現之前,Spring Security 就已經發展了好多年了,但是使用的並不多,安全管理這塊,一直主打的是 Shiro 。
Spring Security 與 Shiro 的區別?
相對於 Shiro 來說,在經典的 SSM/SSH 框架中整合 Spring Security 都是比較繁瑣,雖然 Spring Security 功能比 Shiro 強大,但是因爲配置比較繁瑣,使用的反而沒有 Shiro 多。
兩者不同之處:
1. Spring Security 功能比 Shiro 更加豐富一些;
2. Spring Security 上手複雜;Shiro 上手簡單;
3. Spring Security 依賴 Spring 容器;Shiro 依賴性低,不需要任何框架和容器;
自從有了 Spring Boot 之後,Spring Boot 對於 Spring Security 提供了自動化配置方案,可以零配置使用 Spring Security ,下面看一下具體使用吧!
Spring Security 初體驗?
1. 準備環境以及工具
- JDK 8
- IDEA
2. 創建項目
在 Spring Boot 中使用 Spring Security 非常容易,只需要引入對應依賴即可:
pom.xml 中的 Spring Security 依賴:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
我們創建一個測試接口:
@RestController
public class HelloController {
@GetMapping("/hello")
public String hello() {
return "hello security!";
}
}
啓動項目,通過 URL 訪問 /hello 接口,需要登陸之後才能訪問。
默認的賬戶是:user
默認的密碼是隨機生成的,這裏我們看一下控制檯隨機生成的密碼是多少。
- 好現在我們賬號密碼都知道是多少了,這裏我們去測試登陸。
自定義用戶名或密碼
默認情況下,登陸的用戶名是 user,密碼則是項目啓動時隨機生成的字符串,可以從啓動的控制檯日誌中看到默認密碼,這個隨機生成的密碼,每次啓動都會變,對登陸的用戶名/密碼進行配置,有三種不同的方式:
- 在 application.properties 中進行配置
- 通過 Java 代碼配置到內存中
- 通過 Java 從數據庫中加載
1. 在配置文件中配置用戶名/密碼
可以直接在 application.properties 中進行配置用戶的基本信息:
這裏配置完成後,重啓項目,控制檯就不會隨機生成密碼了,你就可以使用這裏配置的用戶名和密碼登陸了。因爲這裏測試同上,我們就簡略了!
spring.security.user.name=javaboy
spring.security.user.password=123
spring.security.user.roles=admin
2. 通過 Java 配置用戶名/密碼
第二種情況:首先我們需要創建一個 Spring Security 的配置類,繼承 WebSecurityConfigurerAdapter 類,代碼如下:
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Bean
PasswordEncoder passwordEncoder() {
// 表示對密碼進行加密加鹽
return new BCryptPasswordEncoder();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication()
// 這行表示配置了用戶 javaboy 密碼爲 123 角色爲 admin
.withUser("javaboy").password("$2a$10$3pOk/IanPEdCev05Wmew/uTOj96b3KtDc11nCTOHQdjITV0WA4rQW").roles("admin")
.and()
// 這行表示配置了用戶 紅顏禍水 密碼爲 123 角色爲 user
.withUser("紅顏禍水").password("$2a$10$uNB.x3J3ebn8nirQkRdljO5ZtNQMODKghfNA7J/W07XbrmlBdIzr.").roles("user");
}
}
這裏我們在 configure 方法中配置了兩個用戶,用戶的密碼都是加密之後的字符串(明文是 123),從 Spring 5 開始,強制要求密碼要加密,如果非不想加密,可以使用一個早已過期的 PasswordEncoder de 實例 NoOpPasswordEcoder ,但是不建議這麼做,因爲不安全。
NoOpPasswordEcoder 如何使用代碼如下:
@Bean
PasswordEncoder passwordEncoder() {
// 表示不對密碼進行加密操作
return NoOpPasswordEncoder.getInstance();
}
如何實現BCryptPasswordEncoder 對密碼進行加密加鹽
首先在項目測試類中添加如下代碼,並輸入到控制檯:
@Test
void contextLoads() {
for (int i = 0; i < 10; i++) {
// 這裏創建 BCryptPasswordEncoder 的實例
BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
// 對明文密碼加密加鹽並輸入
System.out.println(encoder.encode("123"));
}
}
控制檯結果如下圖所示:
登陸表單配置
對於登陸接口,登陸成功後的響應,登陸失敗後的響應,我們都可以在 WebSecurityConfigurerAdapter 的實現類中進行配置,例如下面這樣:
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Bean
PasswordEncoder passwordEncoder() {
// 表示對密碼進行加密加鹽
return new BCryptPasswordEncoder();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication()
// 這行表示配置了用戶 javaboy 密碼爲 123 角色爲 admin
.withUser("javaboy").password("$2a$10$3pOk/IanPEdCev05Wmew/uTOj96b3KtDc11nCTOHQdjITV0WA4rQW").roles("admin")
.and()
// 這行表示配置了用戶 紅顏禍水 密碼爲 123 角色爲 user
.withUser("紅顏禍水").password("$2a$10$uNB.x3J3ebn8nirQkRdljO5ZtNQMODKghfNA7J/W07XbrmlBdIzr.").roles("user");
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
// 這行表示訪問 /admin/** 需要具備 admin 角色權限
.antMatchers("/admin/**").hasRole("admin")
// 這行表示訪問 /user/** 需要具備 admin 或者 user 角色權限
.antMatchers("/user/**").hasAnyRole("admin","user")
// 這行表示剩下的其他請求只要登陸成功就能訪問
.anyRequest().authenticated()
.and()
// 這行表示配置表單登陸
.formLogin()
// 這行表示處理表單登陸的 URL 爲 doLogin
.loginProcessingUrl("/doLogin")
// 這行表示配置 Security 默認的登陸頁面
.loginPage("/login")
// 自定義登陸名參數爲 uname
.usernameParameter("uname")
// 自定義登陸密碼參數爲 passwd
.passwordParameter("passwd")
// 登陸成功後的處理
.successHandler(new AuthenticationSuccessHandler() {
@Override
public void onAuthenticationSuccess(HttpServletRequest req,
HttpServletResponse resp,
Authentication authentication) throws IOException, ServletException {
resp.setContentType("application/json;charset=utf-8"); // 設置響應頭爲 json 格式
PrintWriter out = resp.getWriter();
Map<String,Object> map = new HashMap<>();
map.put("status",200); // 登陸成功響應碼 200
map.put("msg",authentication.getPrincipal()); // 將登陸成功後的用戶信息返回
out.write(new ObjectMapper().writeValueAsString(map) );
out.flush();
out.close();
}
})
// 登陸失敗後的處理
.failureHandler(new AuthenticationFailureHandler() {
@Override
public void onAuthenticationFailure(HttpServletRequest req,
HttpServletResponse resp,
AuthenticationException e) throws IOException, ServletException {
resp.setContentType("application/json;charset=utf-8"); // 設置響應頭爲 json 格式
PrintWriter out = resp.getWriter();
Map<String,Object> map = new HashMap<>();
map.put("status",401); // 登陸失敗響應碼 401
if (e instanceof LockedException) {
map.put("msg","賬戶被鎖定,登陸失敗!");
} else if (e instanceof BadCredentialsException) {
map.put("msg","用戶名或密碼輸入錯誤,登陸失敗!");
} else if (e instanceof DisabledException) {
map.put("msg","賬戶被禁用,登陸失敗!");
} else if (e instanceof AccountExpiredException) {
map.put("msg","賬戶過期,登陸失敗!");
} else if (e instanceof CredentialsExpiredException) {
map.put("msg","密碼過期,登陸失敗!");
} else {
map.put("msg","登陸失敗!");
}
out.write(new ObjectMapper().writeValueAsString(map) );
out.flush();
out.close();
}
})
// 這行表示只要跟登陸相關的接口直接通過
.permitAll()
.and()
// 註銷
.logout()
// 處理註銷的請求地址
.logoutUrl("/logout")
// 註銷後的處理
.logoutSuccessHandler(new LogoutSuccessHandler() {
@Override
public void onLogoutSuccess(HttpServletRequest req,
HttpServletResponse resp,
Authentication authentication) throws IOException, ServletException {
resp.setContentType("application/json;charset=utf-8"); // 設置響應頭爲 json 格式
PrintWriter out = resp.getWriter();
Map<String,Object> map = new HashMap<>();
map.put("status",200); // 登陸成功響應碼 200
map.put("msg","註銷成功!"); // 將登陸成功後的用戶信息返回
out.write(new ObjectMapper().writeValueAsString(map) );
out.flush();
out.close();
}
})
.and()
// 這行表示關閉 csrf 攻擊,因爲使用 Postman 工具測試,Security 框架會默認以爲 Postman 的請求帶有 csrf 攻擊
.csrf().disable();
}
}
我們可以在 successHandler 方法中,配置登陸成功的回調,如果是前後端分離的開發的話,登陸成功後直接返回 JSON 即可,同理,failureHandler 方法中配置登陸失敗的回調, logoutSuccessHandler 中配置註銷成功的回調。