一儡毕、登錄配置
對于表單登錄簇捍,能配置登錄成功和失敗的跳轉(zhuǎn)和重定向,Spring Security
通過配置可以實(shí)現(xiàn)自定義跳轉(zhuǎn)杜漠、重定向极景,以及用戶未登錄和登錄用戶無權(quán)限的處理。
1.1驾茴、URL配置
1.1.1盼樟、添加依賴
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
1.1.2、自定義登錄頁面
在resources/templates
下編寫簡單test-login.html
登錄頁面(參考官方文檔)锈至,內(nèi)容如下:
<!DOCTYPE html>
<html lang="en" xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
<head>
<meta charset="UTF-8">
<title>登錄頁面</title>
</head>
<body>
<div th:if="${param.error}">
<p>用戶名或密碼無效</p>
</div>
<form th:action="@{/my-login}" method="post">
<div><label> 用戶名 : <input type="text" name="username"/> </label></div>
<div><label> 密碼: <input type="password" name="password"/> </label></div>
<button type="submit" class="btn">登錄</button>
</form>
</body>
</html>
用戶名和密碼名稱默認(rèn)是
username
和password
創(chuàng)建登錄頁面映射Controller
@Controller // 這里使用@Controller,跳轉(zhuǎn)動(dòng)態(tài)頁面
public class PageController {
@GetMapping("/user-login")
public String myLoginPage(){
return "test-login.html";
}
}
1.1.3晨缴、WebSecurityConfig配置
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
// 自定義頁面的路徑不用驗(yàn)證
.antMatchers(HttpMethod.GET, "/user-login").permitAll()
.anyRequest().authenticated()
.and()
.formLogin()
// 設(shè)置自定義登錄的頁面
.loginPage("/user-login")
// 登錄頁表單提交的 action(th:action="@{/my-login}") URL
.loginProcessingUrl("/my-login");
// post請求默認(rèn)需要csrf驗(yàn)證, 這里使用Thymeleaf模板引擎,表單默認(rèn)發(fā)送csrf峡捡,可不用關(guān)閉
//.and()
//.csrf().disable();
}
}
啟動(dòng)程序后喜庞,訪問localhost:8080/hello
,會(huì)跳轉(zhuǎn)到自定義登錄頁面登錄成功棋返,在F12
可以看到自動(dòng)發(fā)送csrf
:
其他的登錄成功和登錄失敗參考上面,配置如下:
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
// 自定義頁面的路徑不用驗(yàn)證
.antMatchers(HttpMethod.GET, "/user-login").permitAll()
// 失敗跳轉(zhuǎn)不用驗(yàn)證
.antMatchers(HttpMethod.GET, "/user-fail").permitAll()
.anyRequest().authenticated()
.and()
.formLogin()
// 設(shè)置自定義登錄的頁面
.loginPage("/user-login")
// 登錄頁表單提交的 action(th:action="@{/my-login}") URL
.loginProcessingUrl("/my-login");
// .usernameParameter("username") // 默認(rèn)就是 username
// .passwordParameter("password") // 默認(rèn)就是 password
/**
* 登錄成功跳轉(zhuǎn):
* 登錄成功雷猪,如果是直接從登錄頁面登錄睛竣,會(huì)跳轉(zhuǎn)到該URL;
* 如果是從其他頁面跳轉(zhuǎn)到登錄頁面求摇,登錄后會(huì)跳轉(zhuǎn)到原來頁面射沟。
* 可設(shè)置true來任何時(shí)候到跳轉(zhuǎn) .defaultSuccessUrl("/hello2", true);
*/
.defaultSuccessUrl("/hello2");
/**
* 登錄成功重定向(和上面二選一)
*/
.successForwardUrl("/hello3")
/**
* 登錄失敗跳轉(zhuǎn),指定的路徑要能匿名訪問
*/
.failureUrl("/login-fail")
/**
* 登錄失敗重定向(和上面二選一)
*/
.failureForwardUrl("/login-fail");
// post請求需要csrf驗(yàn)證, 這里使用Thymeleaf模板引擎与境,表單默認(rèn)發(fā)送csrf验夯,可不用關(guān)閉
//.and()
//.csrf().disable();
}
}
1.2、登錄處理器
上面使用URL進(jìn)行的配置摔刁,都是通過Security默認(rèn)提供的處理器處理的挥转,一般多用于前后端不分離。
Spring Security
的AuthenticationManager
用來處理身份認(rèn)證的請求,處理的結(jié)果分兩種:
- 認(rèn)證成功:結(jié)果由
AuthenticationSuccessHandler
處理 - 認(rèn)證失敯笠ァ:結(jié)果由
AuthenticationFailureHandler
處理党窜。
Spring Security
提供了多個(gè)實(shí)現(xiàn)于AuthenticationSuccessHandler
接口和CustomAuthenticationFailHandler
接口的子類,想自定義處理器借宵,可以實(shí)現(xiàn)接口幌衣,或繼承接口的實(shí)現(xiàn)類來重寫。
1.2.1壤玫、自定義AuthenticationSuccessHandler
AuthenticationSuccessHandler
是身份驗(yàn)證成功處理器的接口豁护,其下有多個(gè)子類:
-
SavedRequestAwareAuthenticationSuccessHandler
:默認(rèn)的成功處理器,默認(rèn)驗(yàn)證成功后欲间,跳轉(zhuǎn)到原路徑楚里。也可通過defaultSuccessUrl()
配置。 -
SimpleUrlAuthenticationSuccessHandler
:SavedRequestAwareAuthenticationSuccessHandler
的父類括改,只有指定defaultSuccessUrl()
時(shí)腻豌,才會(huì)被調(diào)用。作用:清除原路徑嘱能,使用defaultSuccessUrl()
指定的路徑吝梅。如果直接使用該處理器,則總跳轉(zhuǎn)到根路徑惹骂。 -
ForwardAuthenticationSuccessHandler
:請求重定向苏携。只有指定successForwardUrl
時(shí)被用到。
要想自定義成功處理器对粪,可以通過實(shí)現(xiàn)AuthenticationSuccessHandler
接口或繼承其子類SavedRequestAwareAuthenticationSuccessHandler
來實(shí)現(xiàn):
-
實(shí)現(xiàn)
AuthenticationSuccessHandler
接口如果直接返回Json數(shù)據(jù)時(shí)右冻,可以實(shí)現(xiàn)
AuthenticationSuccessHandler
接口:public class CustomAuthenticationSuccessHandler implements AuthenticationSuccessHandler{ @Override public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws ServletException, IOException { response.setContentType("application/json;charset=UTF-8"); response.getWriter().append( new ObjectMapper().createObjectNode() .put("status", 200) .put("msg", "登錄成功") .toString()); } }
-
繼承
SavedRequestAwareAuthenticationSuccessHandler
類如果只是在登錄認(rèn)證后,需要處理數(shù)據(jù)著拭,再跳轉(zhuǎn)回原路徑時(shí)纱扭,可以繼承該類:
public class CustomAuthenticationSuccessHandler2 extends SavedRequestAwareAuthenticationSuccessHandler { @Override public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws ServletException, IOException { // 登錄成功后,進(jìn)行數(shù)據(jù)處理 System.out.println("用戶登錄成功啦@苷凇H槎辍!"); String authenticationStr = objectMapper.writeValueAsString(authentication); System.out.println("用戶登錄信息打颖杀摇:" + authenticationStr); //處理完成后肃叶,跳轉(zhuǎn)回原請求URL super.onAuthenticationSuccess(request, response, authentication); } }
Spring Security
默認(rèn)是使用SavedRequestAwareAuthenticationSuccessHandler
,在配置中修改為自定義的AuthenticationSuccessHandler
:
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
...
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.anyRequest().authenticated()
.and()
.formLogin()
// 配置使用自定義成功處理器
.successHandler(new AuthenticationSuccessHandler());
}
}
1.2.2十嘿、自定義AuthenticationFailureHandler
AuthenticationFailureHandler
是身份認(rèn)證失敗處理器的接口因惭,其下有多個(gè)子類實(shí)現(xiàn):
-
SimpleUrlAuthenticationFailureHandler
:默認(rèn)的失敗處理器,默認(rèn)認(rèn)證失敗后绩衷,跳轉(zhuǎn)到登錄頁路徑加error
參數(shù)蹦魔,如:http://localhost:8080/login?error
激率。可通過failureUrl()
配置版姑。 -
ForwardAuthenticationFailureHandler
:重定向到指定的URL -
DelegatingAuthenticationFailureHandler
:將AuthenticationException
子類委托給不同的AuthenticationFailureHandler
柱搜,意味著可以為AuthenticationException
的不同實(shí)例創(chuàng)建不同的行為 -
ExceptionMappingAuthenticationFailureHandler
:可以根據(jù)不同的AuthenticationException
類型,設(shè)置不同的跳轉(zhuǎn)url
自定義失敗處理器,可以通過實(shí)現(xiàn)AuthenticationFailureHandler
接口或繼承其子類SimpleUrlAuthenticationFailureHandler
來實(shí)現(xiàn):
-
實(shí)現(xiàn)
AuthenticationFailureHandler
接口:public class CustomAuthenticationFailureHandler implements AuthenticationFailureHandler { @Override public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException { response.setContentType("application/json;charset=UTF-8"); response.getWriter().append( new ObjectMapper().createObjectNode() .put("status", 401) .put("msg", "用戶名或密碼錯(cuò)誤") .toString()); } }
-
繼承
SimpleUrlAuthenticationFailureHandler
類public class CustomAuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler { @Override public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException { // 登錄失敗后剥险,進(jìn)行數(shù)據(jù)處理 System.out.println("登錄失敗啦4险骸!表制!"); String exceptionStr = objectMapper.writeValueAsString(exception.getMessage()); System.out.println(exceptionStr); // 跳轉(zhuǎn)原頁面 super.onAuthenticationFailure(request, response, exception); } }
Spring Security
默認(rèn)驗(yàn)證失敗是使用SimpleUrlAuthenticationFailureHandler
健爬,在配置中修改為自定義的AuthenticationFailureHandler
:
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
...
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.anyRequest().authenticated()
.and()
.formLogin()
// 配置使用自定義失敗處理器
.failureHandler(new AuthenticationFailureHandler());
}
}
這里順便提及DelegatingAuthenticationFailureHandler
和ExceptionMappingAuthenticationFailureHandler
的使用:
-
DelegatingAuthenticationFailureHandler
@EnableWebSecurity @EnableGlobalMethodSecurity(prePostEnabled = true) public class WebSecurityConfig extends WebSecurityConfigurerAdapter { ... @Bean public DelegatingAuthenticationFailureHandler delegatingAuthenticationFailureHandler(){ LinkedHashMap<Class<? extends AuthenticationException>, AuthenticationFailureHandler> handlers = new LinkedHashMap<>(); // 登錄失敗時(shí),使用的失敗處理器 handlers.put(BadCredentialsException.class, new BadCredentialsAuthenticationFailureHandler()); // 用戶過期時(shí)么介,使用的失敗處理器 handlers.put(AccountExpiredException.class, new AccountExpiredAuthenticationFailureHandler()); // 用戶被鎖定時(shí)娜遵,使用的失敗處理 handlers.put(LockedException.class, new LockedAuthenticationFailureHandler()); return new DelegatingAuthenticationFailureHandler(handlers, new AuthenticationFailureHandler()); } @Override protected void configure(HttpSecurity http) throws Exception { http .authorizeRequests() .anyRequest().authenticated() .and() .formLogin() // 配置使用自定義失敗處理器 .failureHandler(delegatingAuthenticationFailureHandler()); } }
-
ExceptionMappingAuthenticationFailureHandler
@EnableWebSecurity @EnableGlobalMethodSecurity(prePostEnabled = true) public class WebSecurityConfig extends WebSecurityConfigurerAdapter { ... @Bean public ExceptionMappingAuthenticationFailureHandler exceptionMappingAuthenticationFailureHandler(){ ExceptionMappingAuthenticationFailureHandler handler = new ExceptionMappingAuthenticationFailureHandler(); HashMap<String, String> map = new HashMap<>(); // 登錄失敗時(shí),跳轉(zhuǎn)到 /badCredentials map.put(BadCredentialsException.class.getName(), "/badCredentials"); // 用戶過期時(shí)壤短,跳轉(zhuǎn)到 /accountExpired map.put(AccountExpiredException.class.getName(), "/accountExpired"); // 用戶被鎖定時(shí)设拟,跳轉(zhuǎn)到 /locked map.put(LockedException.class.getName(), "/locked"); handler.setExceptionMappings(map); return handler; } @Override protected void configure(HttpSecurity http) throws Exception { http .authorizeRequests() .anyRequest().authenticated() .and() .formLogin() // 配置使用自定義失敗處理器 .failureHandler(exceptionMappingAuthenticationFailureHandler()); } }
1.3、認(rèn)證入口
AuthenticationEntryPoint
是Spring Security
認(rèn)證入口點(diǎn)接口久脯,在用戶請求處理過程中遇到認(rèn)證異常時(shí)纳胧,使用特定認(rèn)證方式進(jìn)行認(rèn)證。
AuthenticationEntryPoint
內(nèi)置實(shí)現(xiàn)類:
LoginUrlAuthenticationEntryPoint
:根據(jù)配置的登錄頁面url
帘撰,將用戶重定向到該登錄頁面進(jìn)行認(rèn)證跑慕。默認(rèn)的認(rèn)證方式。-
Http403ForbiddenEntryPoint
:設(shè)置響應(yīng)狀態(tài)為403
摧找,不觸發(fā)認(rèn)證核行。通常在預(yù)身份認(rèn)證中設(shè)置在某些情況下,使用Spring Security進(jìn)行授權(quán)蹬耘,但是在訪問該應(yīng)用程序之前芝雪,某些外部系統(tǒng)已經(jīng)對該用戶進(jìn)行了可靠的身份驗(yàn)證。這些情況稱為“預(yù)身份驗(yàn)證(pre-authenticated)”综苔。
HttpStatusEntryPoint
:設(shè)置特定的響應(yīng)狀態(tài)碼惩系,不觸發(fā)認(rèn)證。BasicAuthenticationEntryPoint
:設(shè)置基本(Http Basic
)認(rèn)證休里,在響應(yīng)狀態(tài)碼401
和Header
為WWW-Authenticate:"Basic realm="xxx"
時(shí)使用。DigestAuthenticationEntryPoint
:設(shè)置摘要(Http Digest
)認(rèn)證赃承,在響應(yīng)狀態(tài)碼401
和Header
為WWW-Authenticate:"Digest realm="xxx"
時(shí)使用妙黍。DelegatingAuthenticationEntryPoint
:根據(jù)匹配URI來委托給不同的AuthenticationEntryPoint
,且必須制定一個(gè)默認(rèn)的認(rèn)證方式瞧剖。
1.3.1拭嫁、自定義AuthenticationEntryPoint
-
自定義處理可免,需要新建類實(shí)現(xiàn)該
AuthenticationEntryPoint
接口:public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint { @Override public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException { response.setContentType("application/json;charset=UTF-8"); response.getWriter().append( new ObjectMapper().createObjectNode() .put("status", 401) .put("msg", "未登錄,請登錄后訪問") .toString()); } }
-
WebSecurityConfig
配置:@EnableWebSecurity @EnableGlobalMethodSecurity(prePostEnabled = true) public class WebSecurityConfig extends WebSecurityConfigurerAdapter { ... @Override protected void configure(HttpSecurity http) throws Exception { // 指定未登錄入口點(diǎn) http.exceptionHandling().authenticationEntryPoint(new CustomAuthenticationEntryPoint()); ... } }
其它子類的用法:
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
...
@Bean
public DelegatingAuthenticationEntryPoint delegatingAuthenticationEntryPoint() {
LinkedHashMap<RequestMatcher, AuthenticationEntryPoint> map = new LinkedHashMap<>();
// GET方式請求/test時(shí)做粤,直接返回 403
map.put(new AntPathRequestMatcher("/test", "GET"), new Http403ForbiddenEntryPoint());
// 訪問 /basic時(shí)浇借,直接返回 400 bad request
map.put(new AntPathRequestMatcher("/basic"),
new HttpStatusEntryPoint(HttpStatus.BAD_REQUEST));
DelegatingAuthenticationEntryPoint entryPoint = new DelegatingAuthenticationEntryPoint(map);
// 除了上面兩個(gè) uri 配置指定的認(rèn)證入口,其它默認(rèn)使用 LoginUrlAuthenticationEntryPoint認(rèn)證入口
entryPoint.setDefaultEntryPoint(new LoginUrlAuthenticationEntryPoint("/user-login"));
return entryPoint;
}
@Override
protected void configure(HttpSecurity http) throws Exception {
/**
* Http403ForbiddenEntryPoint 用法
*/
// http.exceptionHandling()
// .authenticationEntryPoint(new Http403ForbiddenEntryPoint());
/**
* HttpStatusEntryPoint 用法
*/
// http.exceptionHandling()
// .authenticationEntryPoint(new HttpStatusEntryPoint(HttpStatus.BAD_REQUEST));
/**
* DelegatingAuthenticationEntryPoint 用法
*/
http.exceptionHandling()
.authenticationEntryPoint(delegatingAuthenticationEntryPoint());
...
}
}
而對于摘要認(rèn)證DigestAuthenticationEntryPoint
怕品,因?yàn)?code>Http摘要認(rèn)證必須基于MD5
或明文妇垢,不能使用其它加密方式,且加密方式是MD5(username:realm:password)
肉康,所以我們需要手動(dòng)加密用戶密碼:
public int addUser(UserInfo userInfo) throws NoSuchAlgorithmException {
String username = userInfo.getUsername();
String password = userInfo.getPassword();
// 加密密碼
MessageDigest md5 = MessageDigest.getInstance("MD5");
String realm = "realm"; // 默認(rèn)是 readlm
String userData = username + ":" + realm + ":" + password;
password = new String(Hex.encode(md5.digest(userData.getBytes())));
userInfo.setPassword(password);
return userMapper.addUser(userInfo);
}
在WebSecurityConfig
配置中:
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private CustomUserDetailsService userDetailsService;
@Bean
public PasswordEncoder passwordEncoder() {
// 因?yàn)橐呀?jīng)使用摘要認(rèn)證MD5加密闯估,不用再加密,所以這里設(shè)置為明文
return new PasswordEncoder() {
@Override
public String encode(CharSequence charSequence) {
return charSequence.toString();
}
@Override
public boolean matches(CharSequence charSequence, String s) {
return s.equals(charSequence.toString());
}
};
}
// 摘要認(rèn)證的過濾器
@Bean
public DigestAuthenticationFilter digestAuthenticationFilter() {
DigestAuthenticationFilter filter = new DigestAuthenticationFilter();
filter.setAuthenticationEntryPoint(digestAuthenticationEntryPoint());//必須配置
filter.setPasswordAlreadyEncoded(true); // 密碼需要加密吼和,設(shè)為true
filter.setUserDetailsService(userDetailsService);//必須配置
return filter;
}
@Bean
public DigestAuthenticationEntryPoint digestAuthenticationEntryPoint() {
DigestAuthenticationEntryPoint point = new DigestAuthenticationEntryPoint();
point.setRealmName("realm");//realm名稱涨薪,默認(rèn)為realm,該名稱和加密密碼的realm一樣
return point;
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http
// 使用摘要認(rèn)證的入口
.exceptionHandling().authenticationEntryPoint(digestAuthenticationEntryPoint())
.and()
.authorizeRequests()
.antMatchers(HttpMethod.POST, "/addUser").permitAll()
.antMatchers("/hello2").permitAll()
.anyRequest().authenticated()
.and()
.csrf().disable()
// 摘要認(rèn)證的過濾器
.addFilter(digestAuthenticationFilter())
}
1.4炫乓、無權(quán)限處理器
自定義處理刚夺,需要新建類實(shí)現(xiàn)該AccessDeniedHandler
接口:
public class CustomAccessDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException e) throws IOException, ServletException {
response.setContentType("application/json;charset=UTF-8");
response.getWriter().append(
new ObjectMapper().createObjectNode()
.put("status", 401)
.put("msg", "無訪問權(quán)限")
.toString());
}
}
WebSecurityConfig
配置:
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
...
@Override
protected void configure(HttpSecurity http) throws Exception {
// 先注釋,用登錄頁面登錄
//http.exceptionHandling().authenticationEntryPoint(new CustomAuthenticationEntryPoint());
http.exceptionHandling().accessDeniedHandler(new CustomAccessDeniedHandler());
http
.authorizeRequests()
.anyRequest().authenticated()
.and()
.formLogin()
.and()
.csrf().disable();
}
}
啟動(dòng)程序末捣,訪問localhost:8080/get-user
侠姑,跳轉(zhuǎn)登錄頁面,輸入用戶名塔粒、密碼登錄后结借,訪問無權(quán)限的資源,會(huì)返回?zé)o權(quán)限Json
信息:
1.5卒茬、記住登錄
Spring Security記住登錄功能有兩種方式:基于瀏覽器的Cookie
存儲(chǔ)和基于數(shù)據(jù)庫的存儲(chǔ)船老。
登錄頁添加記住登錄按鈕
<!DOCTYPE html>
<html lang="en" xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
<head>
<meta charset="UTF-8">
<title>登錄頁面</title>
</head>
<body>
<div th:if="${param.error}">
<p>用戶名或密碼無效</p>
</div>
<form th:action="@{/my-login}" method="post">
<div><label> 用戶名 : <input type="text" name="username"/> </label></div>
<div><label> 密碼: <input type="password" name="password"/> </label></div>
<div>
<label><input type="checkbox" name="remember-me"/>記住登錄</label>
<button type="submit">登錄</button>
</div>
</form>
</body>
</html>
1.5.1、Cookie存儲(chǔ)
WebSecurityConfig配置:
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
//...
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/user-login").permitAll()
.loginProcessingUrl("/my-login")
.successHandler(new CustomAuthenticationSuccessHandler())
.failureHandler(new CustomAuthenticationFailureHandler())
.and()
.rememberMe()
// 即登錄頁面的記住登錄按鈕的參數(shù)名
.rememberMeParameter("remember-me")
// 過期時(shí)間
.tokenValiditySeconds(1800)
.and()
.csrf().disable();
}
}
啟動(dòng)程序圃酵,在勾選記住登錄下進(jìn)行登錄柳畔,cookie信息如下,remember-me的過期時(shí)間內(nèi)郭赐,重啟瀏覽器訪問不用登錄薪韩。
1.5.2、數(shù)據(jù)庫存儲(chǔ)
使用 Cookie
存儲(chǔ)雖然很方便捌锭,但是Cookie
畢竟是保存在客戶端的俘陷,而且 Cookie
的值還與用戶名、密碼這些敏感數(shù)據(jù)相關(guān)观谦,雖然加密拉盾,但是將敏感信息存在客戶端,畢竟不太安全豁状。
Spring security
還提供了另一種更安全的實(shí)現(xiàn)機(jī)制:在客戶端的 Cookie
中捉偏,僅保存一個(gè)無意義的加密串(與用戶名倒得、密碼等敏感數(shù)據(jù)無關(guān)),然后在數(shù)據(jù)庫中保存該加密串-用戶信息的對應(yīng)關(guān)系夭禽,自動(dòng)登錄時(shí)霞掺,用 Cookie
中的加密串,到數(shù)據(jù)庫中驗(yàn)證讹躯,如果通過菩彬,自動(dòng)登錄才算通過。
在 WebSecurityConfig
中注入 dataSource
蜀撑,創(chuàng)建一個(gè) PersistentTokenRepository
的Bean
挤巡,并配置數(shù)據(jù)庫存儲(chǔ)自動(dòng)登錄:
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private DataSource dataSource;
@Bean
public PersistentTokenRepository persistentTokenRepository(){
JdbcTokenRepositoryImpl tokenRepository = new JdbcTokenRepositoryImpl();
tokenRepository.setDataSource(dataSource);
// 啟動(dòng)時(shí)創(chuàng)建表,注意酷麦,創(chuàng)建好表后矿卑,注釋掉
// tokenRepository.setCreateTableOnStartup(true);
return tokenRepository;
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/login").permitAll()
.and()
// 記住登錄
.rememberMe()
// 記住我的數(shù)據(jù)存儲(chǔ),調(diào)用上面寫的方法
.tokenRepository(persistentTokenRepository())
// 過期時(shí)間
.tokenValiditySeconds(1800)
.and()
.csrf().disable();
}
}
二沃饶、session管理
在執(zhí)行認(rèn)證過程之前母廷,Spring Security
將運(yùn)行SecurityContextPersistenceFilter
過濾器負(fù)責(zé)存儲(chǔ)安請求之間的全上下文,上下文根據(jù)策略進(jìn)行存儲(chǔ)糊肤,默認(rèn)為HttpSessionSecurityContextRepository
琴昆,其使用http session
作為存儲(chǔ)器。
對于session
管理馆揉,有三種:
-
session
超時(shí)處理:session
有效的時(shí)間业舍,超時(shí)后刪除 -
session
并發(fā)控制:同個(gè)用戶登錄,是否強(qiáng)制退出前一個(gè)登錄升酣,還是禁止后一個(gè)登錄舷暮。 - 集群
session
管理:默認(rèn)session
是放在單個(gè)服務(wù)器的單個(gè)應(yīng)用里,在集群中噩茄,會(huì)出現(xiàn)在一個(gè)節(jié)點(diǎn)應(yīng)用登錄后下面,session
只能在該節(jié)點(diǎn)使用。另一個(gè)節(jié)點(diǎn)不能使用其他節(jié)點(diǎn)的session
绩聘,還會(huì)需要登錄沥割,所以需要集群共用一個(gè)session
2.1、session超時(shí)
設(shè)置Session
的超時(shí)凿菩,很簡單机杜,只需要在配置文件application.yml
配置即可,如下為設(shè)置50
秒:
-
Springboot2.0
前的版本:
spring:
session:
timeout: 50
-
Springboot2.0
后的版本:
server:
servlet:
session:
timeout: 50
上面設(shè)置Session
失效時(shí)間為50s
衅谷,實(shí)際源碼TomcatEmbeddedServletContainerFactory
類內(nèi)部會(huì)取1分鐘椒拗。源碼內(nèi)部轉(zhuǎn)成分鐘,然后設(shè)置給tomcat
原生的StandardContext
会喝,所以一般設(shè)置為60秒的整數(shù)倍陡叠。
其實(shí)通過上面配置的點(diǎn)擊進(jìn)去源碼發(fā)現(xiàn):
public void setTimeout(Duration timeout) {
this.timeout = timeout;
}
參數(shù)傳入的是Duration
的實(shí)例,Duration
是Java8
新增的肢执,用來計(jì)算日期差值枉阵,并且是被final
聲明,是線程安全的
Duration
轉(zhuǎn)換字符串方式预茄,默認(rèn)為正兴溜;負(fù)以-
開頭,緊接著P
耻陕。
以下字母不區(qū)分大小寫:
-
D
:天 -
T
:天和小時(shí)之間的分隔符 -
H
:小時(shí) -
M
:分鐘 -
S
:秒
每個(gè)單位都必須是數(shù)字拙徽,且時(shí)分秒順序不能亂
比如:
-
P2DT3M5S
:2
天3
分5
秒 -
P3D:
3`天 -
PT3H
:3
小時(shí)
所以上面配置文件中可以寫:
server:
servlet:
session:
timeout: PT50S
2.2、session超時(shí)處理
2.2.1诗宣、超時(shí)跳轉(zhuǎn)URL
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
@Override
protected void configure(HttpSecurity http) throws Exception {
// session無效時(shí)跳轉(zhuǎn)的url
http.sessionManagement().invalidSessionUrl("/session/invalid");
http
.authorizeRequests()
// 需要放行條跳轉(zhuǎn)的url
.antMatchers("/session/invalid").permitAll()
.anyRequest().authenticated()
}
}
}
2.2.2膘怕、超時(shí)處理器
session無效時(shí)的處理策略,優(yōu)先級比上面的高
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private CustomInvalidSessionStrategy invalidSessionStrategy;
@Override
protected void configure(HttpSecurity http) throws Exception {
// 設(shè)置session無效處理策略
http.sessionManagement().invalidSessionStrategy(invalidSessionStrategy);
http
.authorizeRequests()
.antMatchers("/session/invalid").permitAll()
.anyRequest().authenticated()
}
}
處理策略:
@Component
public class CustomInvalidSessionStrategy implements InvalidSessionStrategy {
@Override
public void onInvalidSessionDetected(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {
// 自定義session無效處理
response.setContentType("application/json;charset=UTF-8");
response.getWriter().append("session無效召庞,請重新登錄");
}
}
2.3岛心、session并發(fā)控制
默認(rèn)下,我們可以在不同瀏覽器同時(shí)登錄同一個(gè)用戶篮灼,這樣就會(huì)保存了多個(gè)Session
忘古,而有時(shí),我們需要只能在一處地方登錄诅诱,其他地方的登錄就讓前一個(gè)失效或不能登錄髓堪。
2.3.1、后登錄致前登錄失效
在一個(gè)瀏覽器登錄后娘荡,再到另一個(gè)瀏覽器登錄干旁,再回到前一個(gè)登錄刷新頁面,登錄失效它改。
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.sessionManagement()
// 設(shè)置session無效處理策略
.invalidSessionStrategy(invalidSessionStrategy)
// 設(shè)置同一個(gè)用戶只能有一個(gè)登陸session
.maximumSessions(1);
http
.authorizeRequests()
.anyRequest().authenticated();
}
}
上面設(shè)置maximumSessions
設(shè)置為1
后疤孕,只能有一個(gè)登錄Session
,多個(gè)登錄央拖,后一個(gè)會(huì)把前一個(gè)登錄的Sesson
失效祭阀。
而對于前一個(gè)登錄Sesson
失效后,刷新頁面會(huì)顯示:
This session has been expired (possibly due to multiple concurrent logins being attempted as the same user).
我們也可以自定義失效返回信息鲜戒,有兩種
-
設(shè)置失效session處理URL:
@EnableWebSecurity public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http.sessionManagement() .invalidSessionStrategy(invalidSessionStrategy) .maximumSessions(1) // 其他地方登錄session失效處理URL .expiredUrl("/session/expired"); http .authorizeRequests() // URL不需驗(yàn)證 .antMatchers("/session/expired").permitAll() .anyRequest().authenticated() } }
-
設(shè)置失效session處理策略:
@EnableWebSecurity public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private CustomSessionInformationExpiredStrategy sessionInformationExpiredStrategy; @Override protected void configure(HttpSecurity http) throws Exception { http.sessionManagement() .invalidSessionStrategy(invalidSessionStrategy) .maximumSessions(1) // 其他地方登錄session失效處理策略 .expiredSessionStrategy(sessionInformationExpiredStrategy); http .authorizeRequests() .anyRequest().authenticated() } }
過期策略:
@Component public class CustomSessionInformationExpiredStrategy implements SessionInformationExpiredStrategy { @Override public void onExpiredSessionDetected(SessionInformationExpiredEvent event) throws IOException, ServletException { HttpServletResponse response = event.getResponse(); response.setContentType("application/json;charset=UTF-8"); response.getWriter().write("當(dāng)前用戶已在其他地方登錄..."); } }
2.3.2专控、前登錄禁后登錄
有時(shí),我們在一個(gè)地方登錄正在操作遏餐,不能被打斷伦腐,這時(shí)就要禁止在其他地方登錄導(dǎo)致當(dāng)前的登錄Session
失效。
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.sessionManagement()
.invalidSessionStrategy(invalidSessionStrategy)
.maximumSessions(1)
// 設(shè)置為true失都,即禁止后面其它人的登錄
.maxSessionsPreventsLogin(true)
.expiredSessionStrategy(sessionInformationExpiredStrategy);
http
.authorizeRequests()
.anyRequest().authenticated()
}
}
禁止后登錄后柏蘑,可以通過如下方式判斷異常進(jìn)行用戶通知:
@Component
public class CustomAuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler {
@Autowired
private ObjectMapper objectMapper;
@Override
public void onAuthenticationFailure(HttpServletRequest request,
HttpServletResponse response,
AuthenticationException exception) {
response.setContentType("application/json;charset=utf-8");
if (exception instanceof SessionAuthenticationException){
response.getWriter().write("用戶已在其它地方登錄幸冻,禁止當(dāng)前登錄...");
}
}
}
2.4、集群session管理
在部署應(yīng)用時(shí)咳焚,搭建至少兩臺機(jī)器的集群環(huán)境洽损,防止一臺服務(wù)器出現(xiàn)問題而服務(wù)中斷,這樣在一臺機(jī)器在停止服務(wù)時(shí)革半,另一臺機(jī)器還能繼續(xù)提供服務(wù)碑定。
而使用集群,在基于Session
的身份認(rèn)證就會(huì)導(dǎo)致問題:一個(gè)用戶登錄成功后又官,其Session
存放在A
機(jī)器上延刘,而如果Session
不做其他處理,在用戶操作時(shí)六敬,在負(fù)載均衡下碘赖,可能會(huì)請求發(fā)到B
機(jī)器上,而B
機(jī)器無Session
導(dǎo)致無權(quán)限訪問而需要再次登錄外构。
而解決集群中Session
的管理崖疤,可以把Session
抽取出來為一個(gè)獨(dú)立存儲(chǔ),用戶請求需要Session
時(shí)都會(huì)讀取該存儲(chǔ)Session
Spring
提供有Spring Session
來處理集群Session
管理典勇,需要引入如下依賴:
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
使用redis
作為Session
存儲(chǔ)管理劫哼,而Spring Session
支持以下方式存儲(chǔ)Session
,這里只使用Redis割笙。
public enum StoreType {
REDIS,
MONGODB,
JDBC,
HAZELCAST,
NONE;
private StoreType() {
}
}
在配置文件application.yml中配置Redis:
spring:
session:
store-type: redis # session存儲(chǔ)類型為 redis
redis:
database: 1
host: localhost
port: 6379
# 更新策略权烧,ON_SAVE在調(diào)用#SessionRepository#save(Session)時(shí),在response commit前刷新緩存伤溉,
# IMMEDIATE只要有任何更新就會(huì)刷新緩存
flush-mode: on_save # 默認(rèn)
# 存儲(chǔ)session的密鑰的命名空間
namespace: spring:session #默認(rèn)
以不同的端口啟動(dòng)程序般码,如分別以端口8080
和8081
啟動(dòng)兩個(gè)服務(wù)。訪問8080
端口登錄后乱顾,在訪問8081
就不需要登錄了板祝,說明Session
被共用了。
二走净、退出登錄
默認(rèn)的退出登錄URL
為/logout
券时,如前面登錄的程序,訪問localhost:8080/logout
便退出登錄伏伯,退出登錄后橘洞,默認(rèn)跳轉(zhuǎn)到登錄頁面。
2.1说搅、自定義退出URL
也可通過在WebSecurityConfig
進(jìn)行自定義配置:
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.logout()
// 退出登錄的url, 默認(rèn)為/logout
.logoutUrl("/logout2")
}
}
2.2炸枣、退出成功處理
-
退出成功處理URL:
public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http .logout() // 退出登錄的url, 默認(rèn)為/logout .logoutUrl("/logout2") // 退出成功跳轉(zhuǎn)URL,注意該URL不需要權(quán)限驗(yàn)證 .logoutSuccessUrl("/logout/success").permitAll() } }
-
退出成功處理器
public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http .logout() // 退出登錄的url, 默認(rèn)為/logout .logoutUrl("/logout2") // 退出成功跳轉(zhuǎn)URL,注意該URL不需要權(quán)限驗(yàn)證适肠,所有加.permitAll //.logoutSuccessUrl("/logout/success").permitAll() //退出登錄成功處理器 .logoutSuccessHandler(logoutSuccessHandler) } }
處理器:
@Component public class CustomLogoutSuccessHandler implements LogoutSuccessHandler { @Override public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException { response.setContentType("application/json;charset=utf-8"); response.getWriter().write("退出登錄成功"); } }
2.3霍衫、退出成功刪除Cookie
默認(rèn)退出后不會(huì)刪除Cookie『钛可配置退出后刪除:
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.logout()
// 退出登錄的url, 默認(rèn)為/logout
.logoutUrl("/logout2")
// 退出成功跳轉(zhuǎn)URL慕淡,注意該URL不需要權(quán)限驗(yàn)證,所有加.permitAll
//.logoutSuccessUrl("/logout/success").permitAll()
//退出登錄成功處理器
.logoutSuccessHandler(logoutSuccessHandler)
// 退出登錄刪除指定的cookie
.deleteCookies("JSESSIONID")
}
}