Spring Security 最佳實(shí)踐
來源:juejin.cn/post/7026734817853210661
Spring Security簡介 Spring Security 認(rèn)證流程 Spring Security 項(xiàng)目搭建 導(dǎo)入依賴 訪問頁面 自定義用戶名和密碼 UserDetailsService詳解 PasswordEncoder密碼解析器詳解 登錄配置 角色權(quán)限 403 權(quán)限不足頁面處理 RememberMe(記住我) Spring Security 注解 Spring Security中CSRF 什么是CSRF?
今天來一篇 Spring Security 精講,相信你看過之后能徹底搞懂 Spring Security。
Spring Security簡介
Spring Security 是一種高度自定義的安全框架,利用(基于)SpringIOC/DI和AOP功能,為系統(tǒng)提供了聲明式安全訪問控制功能,「減少了為系統(tǒng)安全而編寫大量重復(fù)代碼的工作」 。
「核心功能:認(rèn)證和授權(quán)」
Spring Security 認(rèn)證流程
SpringSecurity認(rèn)證執(zhí)行流程
Spring Security 項(xiàng)目搭建
導(dǎo)入依賴
Spring Security已經(jīng)被Spring boot進(jìn)行集成,使用時(shí)直接引入啟動器即可
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
訪問頁面
導(dǎo)入spring-boot-starter-security啟動器后,Spring Security已經(jīng)生效,默認(rèn)攔截全部請求,如果用戶沒有登錄,跳轉(zhuǎn)到內(nèi)置登錄頁面。
在瀏覽器輸入:http://localhost:8080/ 進(jìn)入Spring Security內(nèi)置登錄頁面
用戶名:user。
密碼:項(xiàng)目啟動,打印在控制臺中。
自定義用戶名和密碼
修改「application.yml」 文件
# 靜態(tài)用戶,一般只在內(nèi)部網(wǎng)絡(luò)認(rèn)證中使用,如:內(nèi)部服務(wù)器1,訪問服務(wù)器2
spring:
security:
user:
name: test # 通過配置文件,設(shè)置靜態(tài)用戶名
password: test # 配置文件,設(shè)置靜態(tài)登錄密碼
UserDetailsService詳解
什么也沒有配置的時(shí)候,賬號和密碼是由Spring Security定義生成的。而在實(shí)際項(xiàng)目中賬號和密碼都是從數(shù)據(jù)庫中查詢出來的。所以我們要通過「自定義邏輯控制認(rèn)證邏輯」 。如果需要自定義邏輯時(shí),只需要實(shí)現(xiàn)UserDetailsService接口
@Component
public class UserSecurity implements UserDetailsService {
@Autowired
private UserService userService;
@Override
public UserDetails loadUserByUsername(String userName) throws UsernameNotFoundException {
User user = userService.login(userName);
System.out.println(user);
if (null==user){
throw new UsernameNotFoundException("用戶名錯誤");
}
org.springframework.security.core.userdetails.User result =
new org.springframework.security.core.userdetails.User(
userName,user.getPassword(), AuthorityUtils.createAuthorityList()
);
return result;
}
}
PasswordEncoder密碼解析器詳解
PasswordEncoder
「PasswordEncoder」 是SpringSecurity 的密碼解析器,用戶密碼校驗(yàn)、加密 。自定義登錄邏輯時(shí)要求必須給容器注入PaswordEncoder的bean對象
SpringSecurity 定義了很多實(shí)現(xiàn)接口「PasswordEncoder」 滿足我們密碼加密、密碼校驗(yàn) 使用需求。
PasswordEncoder密碼解析器詳解
自定義密碼解析器
編寫類,實(shí)現(xiàn)PasswordEncoder 接口
/**
* 憑證匹配器,用于做認(rèn)證流程的憑證校驗(yàn)使用的類型
* 其中有2個核心方法
* 1. encode - 把明文密碼,加密成密文密碼
* 2. matches - 校驗(yàn)明文和密文是否匹配
* */
public class MyMD5PasswordEncoder implements PasswordEncoder {
/**
* 加密
* @param charSequence 明文字符串
* @return
*/
@Override
public String encode(CharSequence charSequence) {
try {
MessageDigest digest = MessageDigest.getInstance("MD5");
return toHexString(digest.digest(charSequence.toString().getBytes()));
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
return "";
}
}
/**
* 密碼校驗(yàn)
* @param charSequence 明文,頁面收集密碼
* @param s 密文 ,數(shù)據(jù)庫中存放密碼
* @return
*/
@Override
public boolean matches(CharSequence charSequence, String s) {
return s.equals(encode(charSequence));
}
/**
* @param tmp 轉(zhuǎn)16進(jìn)制字節(jié)數(shù)組
* @return 飯回16進(jìn)制字符串
*/
private String toHexString(byte [] tmp){
StringBuilder builder = new StringBuilder();
for (byte b :tmp){
String s = Integer.toHexString(b & 0xFF);
if (s.length()==1){
builder.append("0");
}
builder.append(s);
}
return builder.toString();
}
}
2.在配置類中指定自定義密碼憑證匹配器
/**
* 加密
* @return 加密對象
* 如需使用自定義密碼憑證匹配器 返回自定義加密對象
* 例如: return new MD5PasswordEncoder();
*/
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder(); //Spring Security 自帶
}
登錄配置
方式一 轉(zhuǎn)發(fā)
http.formLogin()
.usernameParameter("name") // 設(shè)置請求參數(shù)中,用戶名參數(shù)名稱。 默認(rèn)username
.passwordParameter("pswd") // 設(shè)置請求參數(shù)中,密碼參數(shù)名稱。 默認(rèn)password
.loginPage("/toLogin") // 當(dāng)用戶未登錄的時(shí)候,跳轉(zhuǎn)的登錄頁面地址是什么? 默認(rèn) /login
.loginProcessingUrl("/login") // 用戶登錄邏輯請求地址是什么。 默認(rèn)是 /login
.failureForwardUrl("/failure"); // 登錄失敗后,請求轉(zhuǎn)發(fā)的位置。Security請求轉(zhuǎn)發(fā)使用Post請求。默認(rèn)轉(zhuǎn)發(fā)到:loginPage?error
.successForwardUrl("/toMain"); // 用戶登錄成功后,請求轉(zhuǎn)發(fā)到的位置。Security請求轉(zhuǎn)發(fā)使用POST請求。
方式二 :重定向
http.formLogin()
.usernameParameter("name") // 設(shè)置請求參數(shù)中,用戶名參數(shù)名稱。 默認(rèn)username
.passwordParameter("pswd") // 設(shè)置請求參數(shù)中,密碼參數(shù)名稱。 默認(rèn)password
.loginPage("/toLogin") // 當(dāng)用戶未登錄的時(shí)候,跳轉(zhuǎn)的登錄頁面地址是什么? 默認(rèn) /login
.loginProcessingUrl("/login") // 用戶登錄邏輯請求地址是什么。 默認(rèn)是 /login
.defaultSuccessUrl("/toMain",true); //用戶登錄成功后,響應(yīng)重定向到的位置。GET請求。必須配置絕對地址。
.failureUrl("/failure"); // 登錄失敗后,重定向的位置。
方式三:自定義登錄處理器
自定義登錄失敗邏輯處理器
/*自定義登錄失敗處理器*/
public class MyAuthenticationFailureHandler implements AuthenticationFailureHandler {
private String url;
private boolean isRedirect;
public MyAuthenticationFailureHandler(String url, boolean isRedirect) {
this.url = url;
this.isRedirect = isRedirect;
}
@Override
public void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
if (isRedirect){
httpServletResponse.sendRedirect(url);
}else {
httpServletRequest.getRequestDispatcher(url).forward(httpServletRequest,httpServletResponse);
}
}
//get set 方法 省略
自定義登錄成功邏輯處理器
/**
* 自定義登錄成功后處理器
* 轉(zhuǎn)發(fā)重定向,有代碼邏輯實(shí)現(xiàn)
* */
public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
private String url;
private boolean isRedirect;
public MyAuthenticationSuccessHandler(String url, boolean isRedirect) {
this.url = url;
this.isRedirect = isRedirect;
}
/**
* @param request 請求對象 request.getRequestDispatcher.forward()
* @param response 響應(yīng)對象 response.sendRedirect()
* @param authentication 用戶認(rèn)證成功后的對象。其中報(bào)換用戶名權(quán)限結(jié)合,內(nèi)容是
* 自定義UserDetailsService
* */
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
if (isRedirect){
response.sendRedirect(url);
}else {
request.getRequestDispatcher(url).forward(request,response);
}
}
//get set 方法 省略
http.formLogin()
.usernameParameter("name") // 設(shè)置請求參數(shù)中,用戶名參數(shù)名稱。 默認(rèn)username
.passwordParameter("pswd") // 設(shè)置請求參數(shù)中,密碼參數(shù)名稱。 默認(rèn)password
.loginPage("/toLogin") // 當(dāng)用戶未登錄的時(shí)候,跳轉(zhuǎn)的登錄頁面地址是什么? 默認(rèn) /login
.loginProcessingUrl("/login") // 用戶登錄邏輯請求地址是什么。 默認(rèn)是 /login
登錄相關(guān)配置類
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserSecurity userSecurity;
@Autowired
private PersistentTokenRepository persistentTokenRepository;
/**
* 加密
* @return 加密對象
* 如需使用自定義加密邏輯 返回自定義加密對象
* return new MD5PasswordEncoder(); return new SimplePasswordEncoder();
*/
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder(); //Spring Security 自帶
}
@Override
protected void configure(HttpSecurity http) throws Exception {
// 配置登錄請求相關(guān)內(nèi)容。
http.formLogin()
.loginPage("/toLogin") // 當(dāng)用戶未登錄的時(shí)候,跳轉(zhuǎn)的登錄頁面地址是什么? 默認(rèn) /login
.usernameParameter("name") // 設(shè)置請求參數(shù)中,用戶名參數(shù)名稱。 默認(rèn)username
.passwordParameter("pswd") // 設(shè)置請求參數(shù)中,密碼參數(shù)名稱。 默認(rèn)password
.loginProcessingUrl("/login") //設(shè)置登錄 提交表單數(shù)據(jù)訪問請求地址
.defaultSuccessUrl("/toMain")
.failureUrl("/toLogin");
//.successForwardUrl("/toMain")
//.failureForwardUrl("/toLogin");
//.successHandler(new LoginSuccessHandler("/toMain", true)) //自定義登錄成功處理器
//.failureHandler(new LoginErrorHandler("/toLogin", true));
http.authorizeRequests()
//.antMatchers("/toLogin").anonymous() //只能匿名用戶訪問
.antMatchers("/toLogin", "/register", "/login", "/favicon.ico").permitAll() // /toLogin請求地址,可以隨便訪問。
.antMatchers("/**/*.js").permitAll() // 授予所有目錄下的所有.js文件可訪問權(quán)限
.regexMatchers(".*[.]css").permitAll() // 授予所有目錄下的所有.css文件可訪問權(quán)限
.anyRequest().authenticated(); // 任意的請求,都必須認(rèn)證后才能訪問。
// 配置退出登錄
http.logout()
.invalidateHttpSession(true) // 回收HttpSession對象。退出之前調(diào)用HttpSession.invalidate() 默認(rèn) true
.clearAuthentication(true) // 退出之前,清空Security記錄的用戶登錄標(biāo)記。 默認(rèn) true
// .addLogoutHandler() // 增加退出處理器。
.logoutSuccessUrl("/") // 配置退出后,進(jìn)入的請求地址。 默認(rèn)是loginPage?logout
.logoutUrl("/logout"); // 配置退出登錄的路徑地址。和頁面請求地址一致即可。
// 關(guān)閉CSRF安全協(xié)議。
// 關(guān)閉是為了保證完整流程的可用。
http.csrf().disable();
}
@Bean
public PersistentTokenRepository persistentTokenRepository(DataSource dataSource){
JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl();
jdbcTokenRepository.setDataSource(dataSource);
//jdbcTokenRepository.setCreateTableOnStartup(true);
return jdbcTokenRepository;
}
}
角色權(quán)限
?
「hasAuthority(String)」 判斷角色是否具有特定權(quán)限
?
http.authorizeRequests().antMatchers("/main1.html").hasAuthority("admin")
?
「hasAnyAuthority(String ...)」 如果用戶具備給定權(quán)限中某一個,就允許訪問
?
http.authorizeRequests().antMatchers("/admin/read").hasAnyAuthority("xxx","xxx")
?
「hasRole(String)」 如果用戶具備給定角色就允許訪問。否則出現(xiàn)403
?
//請求地址為/admin/read的請求,必須登錄用戶擁有'管理員'角色才可訪問
http.authorizeRequests().antMatchers("/admin/read").hasRole("管理員")
?
「hasAnyRole(String ...)」 如果用戶具備給定角色的任意一個,就允許被訪問
?
//用戶擁有角色是管理員 或 訪客 可以訪問 /guest/read
http.authorizeRequests().antMatchers("/guest/read").hasAnyRole("管理員", "訪客")
?
「hasIpAddress(String)」 請求是指定的IP就運(yùn)行訪問
?
//ip 是127.0.0.1 的請求 可以訪問/ip
http.authorizeRequests().antMatchers("/ip").hasIpAddress("127.0.0.1")
403 權(quán)限不足頁面處理
1.編寫類實(shí)現(xiàn)接口「AccessDeniedHandler」
/**
* @describe 403 權(quán)限不足
* @author: AnyWhere
* @date 2021/4/18 20:57
*/
@Component
public class MyAccessDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException e)
throws IOException, ServletException {
response.setStatus(HttpServletResponse.SC_OK);
response.setContentType("text/html;charset=UTF-8");
response.getWriter().write(
"<html>" +
"<body>" +
"<div style='width:800px;text-align:center;margin:auto;font-size:24px'>" +
"權(quán)限不足,請聯(lián)系管理員" +
"</div>" +
"</body>" +
"</html>"
);
response.getWriter().flush();//刷新緩沖區(qū)
}
}
2.配置類中配置exceptionHandling
// 配置403訪問錯誤處理器。
http.exceptionHandling().accessDeniedHandler(myAccessDeniedHandler);/
RememberMe(記住我)
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
//配置記住密碼
http.rememberMe()
.rememberMeParameter("remember-me") // 修改請求參數(shù)名。 默認(rèn)是remember-me
.tokenValiditySeconds(14*24*60*60) // 設(shè)置記住我有效時(shí)間。單位是秒。默認(rèn)是14天
.rememberMeCookieName("remember-me") // 修改remember me的cookie名稱。默認(rèn)是remember-me
.tokenRepository(persistentTokenRepository) // 配置用戶登錄標(biāo)記的持久化工具對象。
.userDetailsService(userSecurity); // 配置自定義的UserDetailsService接口實(shí)現(xiàn)類對象
}
@Bean
public PersistentTokenRepository persistentTokenRepository(DataSource dataSource){
JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl();
jdbcTokenRepository.setDataSource(dataSource);
//jdbcTokenRepository.setCreateTableOnStartup(true);
return jdbcTokenRepository;
}
}
Spring Security 注解
@Secured
?
角色校驗(yàn) ,請求到來訪問控制單元方法時(shí)必須包含XX角色才能訪問
角色必須添加ROLE_前綴
?
@Secured({"ROLE_管理員","ROLE_訪客"})
@RequestMapping("/toMain")
public String toMain(){
return "main";
}
使用注解@Secured需要在配置類中添加注解 使@Secured注解生效
@EnableGlobalMethodSecurity(securedEnabled = true)
@PreAuthorize
?
權(quán)限檢驗(yàn),請求到來訪問控制單元之前必須包含xx權(quán)限才能訪問,控制單元方法執(zhí)行前進(jìn)行角色校驗(yàn)
?
/**
* [ROLE_管理員, admin:read, admin:write, all:login, all:logout, all:error, all:toMain]
* @PreAuthorize 角色 、權(quán)限 校驗(yàn) 方法執(zhí)行前進(jìn)行角色校驗(yàn)
*
* hasAnyAuthority()
* hasAuthority()
*
* hasPermission()
*
*
* hasRole()
* hasAnyRole()
* */
@PreAuthorize("hasAnyRole('ROLE_管理員','ROLE_訪客')")
@RequestMapping("/toMain")
@PreAuthorize("hasAuthority('admin:write')")
public String toMain(){
return "main";
}
使用@PreAuthorize和@PostAuthorize 需要在配置類中配置注解@EnableGlobalMethodSecurity 才能生效
@EnableGlobalMethodSecurity(prePostEnabled = true)
@PostAuthorize
?
權(quán)限檢驗(yàn),請求到來訪問控制單元之后必須包含xx權(quán)限才能訪問 ,控制單元方法執(zhí)行完后進(jìn)行角色校驗(yàn)
?
/**
* [ROLE_管理員, admin:read, admin:write, all:login, all:logout, all:error, all:toMain]
* @PostAuthorize 角色 、權(quán)限 校驗(yàn) 方法執(zhí)行后進(jìn)行角色校驗(yàn)
*
* hasAnyAuthority()
* hasAuthority()
* hasPermission()
* hasRole()
* hasAnyRole()
* */
@PostAuthorize("hasRole('ROLE_管理員')")
@RequestMapping("/toMain")
@PreAuthorize("hasAuthority('admin:write')")
public String toMain(){
return "main";
}
Spring Security 整合Thymeleaf 進(jìn)行權(quán)限校驗(yàn)
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.thymeleaf.extras</groupId>
<artifactId>thymeleaf-extras-springsecurity5</artifactId>
</dependency>
Spring Security中CSRF
什么是CSRF?
CSRF(Cross-site request forgery)跨站請求偽造,也被稱為“One Click Attack” 或者Session Riding。通過偽造用戶請求訪問受信任站點(diǎn)的非法請求訪問。
跨域:只要網(wǎng)絡(luò)協(xié)議,ip地址,端口中任何一個不相同就是跨域請求。
客戶端與服務(wù)進(jìn)行交互時(shí),由于http協(xié)議本身是無狀態(tài)協(xié)議,所以引入了cookie進(jìn)行記錄客戶端身份。在cookie中會存放session id用來識別客戶端身份的。在跨域的情況下,session id可能被第三方惡意劫持,通過這個session id向服務(wù)端發(fā)起請求時(shí),服務(wù)端會認(rèn)為這個請求是合法的,可能發(fā)生很多意想不到的事情。
通俗解釋:
CSRF就是別的網(wǎng)站非法獲取我們網(wǎng)站Cookie值,我們項(xiàng)目服務(wù)器是無法區(qū)分到底是不是我們的客戶端,只有請求中有Cookie,認(rèn)為是自己的客戶端,所以這個時(shí)候就出現(xiàn)了CSRF。
記得點(diǎn)「贊」和「在看」↓
愛你們
