Form表單登錄
默認登錄
1.pom配置
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency>
2.java類
@RestController public class HelloController { @GetMapping("/hello") public String hello() { return "hello"; } }
3.啟動項目
可以發現日志打印了了密碼默認是user用戶
3.訪問
localhost:8080/hello 重定向到了http://localhost:8080/login
3.輸入用戶名密碼再訪問
用戶名:user 密碼:7e6a1360-1115-4eb6-8e17-5308164e5b26
4.默認生成的生成的用戶名和密碼如何生成
查看UserDetailsServiceAutoConfiguration此類
org.springframework.boot.autoconfigure.security.servlet.UserDetailsServiceAutoConfiguration#getOrDeducePassword
private String getOrDeducePassword(User user, PasswordEncoder encoder) { //user獲得密碼
String password = user.getPassword(); if (user.isPasswordGenerated()) {
//這里就是控制台打印的日志 logger.info(String.format("%n%nUsing generated security password: %s%n", user.getPassword())); } return encoder == null && !PASSWORD_ALGORITHM_PATTERN.matcher(password).matches() ? "{noop}" + password : password; }
5.我們再看User
這里一目了然 默認是user和uuid生成用戶名和密碼,我們可以通過在配置文件配置修改
spring.security.user.name=liqiang
spring.security.user.password=liqiang
@ConfigurationProperties( prefix = "spring.security"//說明我們可以通過配置文件配置密碼和user ) public class SecurityProperties { private SecurityProperties.User user = new SecurityProperties.User(); public static class User { //用戶 默認user private String name = "user"; //使用uuid生成密碼 private String password = UUID.randomUUID().toString(); private List<String> roles = new ArrayList(); private boolean passwordGenerated = true; public void setPassword(String password) { //密碼是否不為空 if (StringUtils.hasLength(password)) { //標簽打為false 后面if (user.isPasswordGenerated()) 打印日志 this.passwordGenerated = false; this.password = password; } } } }
內存中多用戶配置
@Configuration public class SecurityConfig extends WebSecurityConfigurerAdapter { /** * 對密碼進行加密的實例 * @return */ @Bean PasswordEncoder passwordEncoder() { /** * 不加密所以使用NoOpPasswordEncoder * 更多可以參考PasswordEncoder 的默認實現官方推薦使用: BCryptPasswordEncoder,BCryptPasswordEncoder */ return NoOpPasswordEncoder.getInstance(); } @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { /** * inMemoryAuthentication 開啟在內存中定義用戶 * 多個用戶通過and隔開 */ auth.inMemoryAuthentication() .withUser("liqiang").password("liqiang").roles("admin") .and() .withUser("admin").password("admin").roles("admin"); } }
PasswordEncoder為防止密碼泄露,數據庫保存的是加密的密碼,然后前端登錄傳過來加密后根據數據庫的進行匹配 我們可以自己實現和用默認的
自定義登錄頁面
security的默認登錄頁面不能滿足我們需求,我們大多數場景都需要自定義登錄頁面
1.增加自定義登錄頁面html
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body>
<!--會get請求login.html 提交則是post login--> <form action="/login.html" method="post"> <div class="input"> <label for="name">用戶名</label> <input type="text" name="username" id="name"> <span class="spin"></span> </div> <div class="input"> <label for="pass">密碼</label> <input type="password" name="password" id="pass"> <span class="spin"></span> </div> <div class="button login"> <button type="submit"> <span>登錄</span> <i class="fa fa-check"></i> </button> </div> </form> </body> </html>
2.配置自定義html頁面路徑
@Configuration public class SecurityConfig extends WebSecurityConfigurerAdapter { /** * 對於不需要授權的靜態文件放行 * @param web * @throws Exception */ @Override public void configure(WebSecurity web) throws Exception { web.ignoring().antMatchers("/js/**", "/css/**", "/images/**"); } @Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests() .anyRequest().authenticated() .and() .formLogin()//form表單的方式 .loginPage("/login.html")//登錄頁面路徑 .permitAll()//不攔截 .and() .csrf()//記得關閉 .disable(); } }@Configuration public class SecurityConfig extends WebSecurityConfigurerAdapter { /** * 對於不需要授權的靜態文件放行 * @param web * @throws Exception */ @Override public void configure(WebSecurity web) throws Exception { web.ignoring().antMatchers("/js/**", "/css/**", "/images/**"); } @Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests() .anyRequest().authenticated() .and() .formLogin()//form表單的方式 .loginPage("/login.html")//登錄頁面路徑 .permitAll()//不攔截 .and() .csrf()//記得關閉 .disable(); } }
3.再次訪問就是顯示自定義的登錄頁面
自定義登錄請求地址
1.如果我們引入security 什么都不做那么spring securty的默認的登錄頁面和登錄地址為以下,我們可以手動修改
get http://localhost:8080/login 登錄頁面
post http://localhost:8080/login 登錄請求
我們前面自定義了登錄頁面請求但是請求地址action配置的是否可以自定義為其他
<form action="/login.html" method="post">
2.我們可以通過后台配置
@Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests() .anyRequest().authenticated() .and() .formLogin() .loginPage("/login.html")//登錄頁面路徑 .loginProcessingUrl("/doLogin") .permitAll()//不攔截 .and() .csrf()//記得關閉 .disable(); }
我們的form表單action配置就可以改為
<form action="/doLogin" method="post">
登錄url自定義和處理登錄的源碼處
默認配置是在哪里配置的呢可以看org.springframework.security.config.annotation.web.configurers.FormLoginConfigurer
public FormLoginConfigurer() {
//<1>調用父類的 詳情看里面 super(new UsernamePasswordAuthenticationFilter(), (String)null);
//用戶名密碼默認參數名 this.usernameParameter("username"); this.passwordParameter("password"); }
<1>org.springframework.security.config.annotation.web.configurers.AbstractAuthenticationFilterConfigurer#AbstractAuthenticationFilterConfigurer()
protected AbstractAuthenticationFilterConfigurer(F authenticationFilter, String defaultLoginProcessingUrl) {
<2>
this(); this.authFilter = authenticationFilter; if (defaultLoginProcessingUrl != null) { this.loginProcessingUrl(defaultLoginProcessingUrl); } }
<2>org.springframework.security.config.annotation.web.configurers.AbstractAuthenticationFilterConfigurer.AbstractAuthenticationFilterConfigurer
protected AbstractAuthenticationFilterConfigurer() { this.defaultSuccessHandler = new SavedRequestAwareAuthenticationSuccessHandler(); this.successHandler = this.defaultSuccessHandler;
//loginPage的默認值是/login this.setLoginPage("/login"); }
4.默認登錄請求路徑的源碼處
org.springframework.security.config.annotation.web.configurers.FormLoginConfigurer#init
public void init(H http) throws Exception {
//<1>調用了父類的init super.init(http); this.initDefaultLoginFilter(http); }
<1>org.springframework.security.config.annotation.web.configurers.AbstractAuthenticationFilterConfigurer#init
public void init(B http) throws Exception {
<2> this.updateAuthenticationDefaults(); this.updateAccessDefaults(http); this.registerDefaultAuthenticationEntryPoint(http); }
<2>
org.springframework.security.config.annotation.web.configurers.AbstractAuthenticationFilterConfigurer#updateAuthenticationDefaults
protected final void updateAuthenticationDefaults() { //可以看到本質是使用使用loginProcessingUrl,如果我們沒有手動設置才使用默認的loginPage if (this.loginProcessingUrl == null) { this.loginProcessingUrl(this.loginPage); } if (this.failureHandler == null) { this.failureUrl(this.loginPage + "?error"); } LogoutConfigurer<B> logoutConfigurer = (LogoutConfigurer)((HttpSecurityBuilder)this.getBuilder()).getConfigurer(LogoutConfigurer.class); if (logoutConfigurer != null && !logoutConfigurer.isCustomLogoutSuccess()) { logoutConfigurer.logoutSuccessUrl(this.loginPage + "?logout"); } }
自定義登錄用戶名密碼參數
1.默認情況下 用戶名為username 密碼為password為必須
<div class="input"> <label for="name">用戶名</label> <input type="text" name="username" id="name"> <span class="spin"></span> </div> <div class="input"> <label for="pass">密碼</label> <input type="password" name="password" id="pass"> <span class="spin"></span> </div>
2.我們可以通過配置修改
@Configuration public class SecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests() .anyRequest().authenticated() .and() .formLogin() .loginPage("/login.html")//登錄頁面路徑 .loginProcessingUrl("/doLogin") .usernameParameter("loginName") .passwordParameter("loginPassword") .permitAll()//不攔截 .and() .csrf()//記得關閉 .disable(); } }
自定義登錄用戶名密碼參數源碼處
org.springframework.security.config.annotation.web.configurers.FormLoginConfigurer
public FormLoginConfigurer() { super(new UsernamePasswordAuthenticationFilter(), (String)null);
//設置參數名字 this.usernameParameter("username"); this.passwordParameter("password"); }
2.可以發現最終是調用UsernamePasswordAuthenticationFilter對象的set方法 所以我們可以通過build的時候直接調用覆蓋
org.springframework.security.config.annotation.web.configurers.FormLoginConfigurer#usernameParameter
org.springframework.security.config.annotation.web.configurers.FormLoginConfigurer#passwordParameter
public FormLoginConfigurer<H> usernameParameter(String usernameParameter) {
((UsernamePasswordAuthenticationFilter)this.getAuthenticationFilter()).setUsernameParameter(usernameParameter); return this; } public FormLoginConfigurer<H> passwordParameter(String passwordParameter) { ((UsernamePasswordAuthenticationFilter)this.getAuthenticationFilter()).setPasswordParameter(passwordParameter); return this; }
3.fitler內部獲取用戶名密碼根據配置的usernameParameter和passwordParameter獲取用戶名密碼
org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter#attemptAuthentication
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException { if (this.postOnly && !request.getMethod().equals("POST")) { throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod()); } else {
//<1> String username = this.obtainUsername(request); String password = this.obtainPassword(request); if (username == null) { username = ""; } if (password == null) { password = ""; } username = username.trim(); UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password); this.setDetails(request, authRequest); return this.getAuthenticationManager().authenticate(authRequest); } }
<1>
@Nullable protected String obtainPassword(HttpServletRequest request) {
//通過我們配置的名字獲取 return request.getParameter(this.passwordParameter); } @Nullable protected String obtainUsername(HttpServletRequest request) {
//通過我們配置的名字獲取 return request.getParameter(this.usernameParameter); }
自定義登錄跳轉
1.security可通過defaultSuccessUrl、successForwardUrl指定登錄成功跳轉頁面
defaultSuccessUrl 如果是通過訪問其他頁面無權訪問重定向到登錄頁面登錄,登錄成功則跳轉到來源頁面,如果是直接訪問登錄頁面則直接跳轉到指定url
successForwardUrl 登錄成功不管來源直接跳轉到指定頁面 (注意 這接口采用的服務單轉發的方式而不是302重定向)
@Configuration public class SecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests() .anyRequest().authenticated() .and() .formLogin() .loginPage("/login.html")//登錄頁面路徑 .loginProcessingUrl("/doLogin") .usernameParameter("loginName") .passwordParameter("loginPassword") .defaultSuccessUrl("/index") .successForwardUrl("/index") .permitAll()//不攔截 .and() .csrf()//記得關閉 .disable(); }
自定義登錄失敗跳轉
failureForwardUrl 登錄失敗服務器重定向
failureUrl 登錄失敗前端302重定向地址
@Configuration public class SecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests() .anyRequest().authenticated() .and() .formLogin() .loginPage("/login.html")//登錄頁面路徑 .loginProcessingUrl("/doLogin") .usernameParameter("loginName") .passwordParameter("loginPassword") .defaultSuccessUrl("/index") .successForwardUrl("index") .failureForwardUrl("/loginFail") .failureUrl("/login.html") .permitAll()//不攔截 .and() .csrf()//記得關閉 .disable(); } }
注銷登錄
.and() .logout() .logoutUrl("/logout")//自定義注銷地址 .logoutRequestMatcher(new AntPathRequestMatcher("/logout","POST")) //自定義請求方式 .logoutSuccessUrl("/login.html") //注銷后跳轉頁面 .deleteCookies()//清除cookie .clearAuthentication(true)//清除權限相關 .invalidateHttpSession(true)//清除session .permitAll() .and()
ajax登錄+驗證碼登錄
1.自定義登錄處理器
public class CustomizeUsernamePasswordAuthenticationFilter extends UsernamePasswordAuthenticationFilter { @Override public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException { if (!request.getMethod().equals("POST")) { throw new AuthenticationServiceException( "Authentication method not supported: " + request.getMethod()); } //從session獲得驗證碼 String verify_code = (String) request.getSession().getAttribute("verify_code"); //判斷是否是json post請求 如果不是則走父類的form登錄 if (request.getContentType().equals(MediaType.APPLICATION_JSON_VALUE) || request.getContentType().equals(MediaType.APPLICATION_JSON_UTF8_VALUE)) { Map<String, String> loginData = new HashMap<>(); try { //解析bodyjson數據轉為Map loginData = new ObjectMapper().readValue(request.getInputStream(), Map.class); } catch (IOException e) { throw new AuthenticationServiceException("系統異常"); } String code = loginData.get("code"); //檢查驗證碼 checkCode(response, code, verify_code); //獲得用戶輸入的用戶名和密碼 如果是form登錄的話上面配置的用戶名key和密碼key就不會起效 用默認的 String username = loginData.get(getUsernameParameter()); String password = loginData.get(getPasswordParameter()); if (username == null) { username = ""; } if (password == null) { password = ""; } username = username.trim(); //模擬父類的實現 UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken( username, password); setDetails(request, authRequest); return this.getAuthenticationManager().authenticate(authRequest); } else { checkCode(response, request.getParameter("code"), verify_code); //如果是form登錄直接使用父類的 return super.attemptAuthentication(request, response); } } public void checkCode(HttpServletResponse resp, String code, String verify_code) { if (code == null || verify_code == null || "".equals(code) || !verify_code.toLowerCase().equals(code.toLowerCase())) { //驗證碼不正確 throw new AuthenticationServiceException("驗證碼不正確"); } } }
2.初始化登錄處理器並重寫登錄成功和登錄失敗的邏輯
com.liqiang.demo.configs.SecurityConfig#initLoginFilter
public Filter initLoginFilter() throws Exception { CustomizeUsernamePasswordAuthenticationFilter customizeUsernamePasswordAuthenticationFilter= new CustomizeUsernamePasswordAuthenticationFilter(); /** * 授權成功處理器 可以看父類里面默認就是這2個 */ SavedRequestAwareAuthenticationSuccessHandler handler=new SavedRequestAwareAuthenticationSuccessHandler(); AuthenticationFailureHandler failureHandler = new SimpleUrlAuthenticationFailureHandler(); customizeUsernamePasswordAuthenticationFilter.setAuthenticationSuccessHandler(new AuthenticationSuccessHandler() { @Override public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException { //此處我們做了兼容 如果是json 則響應json 否則走form默認的邏輯 if (request.getContentType().equals(MediaType.APPLICATION_JSON_VALUE) || request.getContentType().equals(MediaType.APPLICATION_JSON_UTF8_VALUE)) { response.setContentType("application/json;charset=utf-8"); PrintWriter out = response.getWriter(); Map<String,Object> result=new HashMap<>(); result.put("code",200); result.put("message","登錄成功"); String s = new ObjectMapper().writeValueAsString(result); out.write(s); out.flush(); out.close(); }else{ //表單登錄委托給默認的處理器 handler.onAuthenticationSuccess(request,response,authentication); } } }); //授權失敗處理器 customizeUsernamePasswordAuthenticationFilter.setAuthenticationFailureHandler(new AuthenticationFailureHandler() { @Override public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException { //此處我們做了兼容 如果是json 則響應json 否則走form默認的邏輯 if (request.getContentType().equals(MediaType.APPLICATION_JSON_VALUE) || request.getContentType().equals(MediaType.APPLICATION_JSON_UTF8_VALUE)) { response.setContentType("application/json;charset=utf-8"); PrintWriter out = response.getWriter(); Map<String,Object> result=new HashMap<>(); result.put("code",200); result.put("message","登錄失敗"); if (exception instanceof LockedException) { result.put("code",901); result.put("message","賬戶被鎖定,請聯系管理員!"); } else if (exception instanceof CredentialsExpiredException) { result.put("code",902); result.put("message","密碼過期,請聯系管理員!"); } else if (exception instanceof AccountExpiredException) { result.put("code",903); result.put("message","賬戶過期,請聯系管理員!"); } else if (exception instanceof DisabledException) { result.put("code",904); result.put("message","賬戶被禁用,請聯系管理員!"); } else if (exception instanceof BadCredentialsException) { result.put("code",905); result.put("message","用戶名或者密碼輸入錯誤,請重新輸入!"); } out.write(new ObjectMapper().writeValueAsString(result)); out.flush(); out.close(); }else{ //表單登錄委托給默認的處理器 failureHandler.onAuthenticationFailure(request,response,exception); } } }); customizeUsernamePasswordAuthenticationFilter.setAuthenticationManager(super.authenticationManagerBean()); //攔截處理的url customizeUsernamePasswordAuthenticationFilter.setFilterProcessesUrl("/doLogin"); return customizeUsernamePasswordAuthenticationFilter; }
3.替換默認的登錄處理器
@Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests() .anyRequest().authenticated() .and() .formLogin() .loginPage("/login.html")//登錄頁面路徑 .loginProcessingUrl("/doLogin") .usernameParameter("loginName") .passwordParameter("loginPassword") .defaultSuccessUrl("/index") .successForwardUrl("/index") .failureForwardUrl("/loginFail") .failureUrl("/login.html") .permitAll()//不攔截 .and() //替換默認的登錄處理器 注意form的配置將不起效果 可看formLogin源碼 並沒有替換 比如 usernameParameter passwordParameter .addFilterAt(initLoginFilter(), UsernamePasswordAuthenticationFilter.class) .csrf()//記得關閉 .disable(); }
4.測試
自動登錄
簡單實用
1.在原來的基礎上增加記住密碼配置
@Configuration public class SecurityConfig extends WebSecurityConfigurerAdapter { /** * 對於不需要授權的靜態文件放行 * @param web * @throws Exception */ @Override public void configure(WebSecurity web) throws Exception { web.ignoring().antMatchers("/js/**", "/css/**", "/images/**"); } /** * 對密碼進行加密的實例 * @return */ @Bean PasswordEncoder passwordEncoder() { /** * 不加密所以使用NoOpPasswordEncoder * 更多可以參考PasswordEncoder 的默認實現官方推薦使用: BCryptPasswordEncoder,BCryptPasswordEncoder */ return NoOpPasswordEncoder.getInstance(); } @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { /** * inMemoryAuthentication 開啟在內存中定義用戶 * 多個用戶通過and隔開 */ auth.inMemoryAuthentication() .withUser("liqiang").password("liqiang").roles("admin") .and() .withUser("admin").password("admin").roles("admin"); } @Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests() .anyRequest().authenticated() .and().rememberMe()//記住登錄 .and() .formLogin()//form表單的方式 .loginPage("/login.html")//登錄頁面路徑 .loginProcessingUrl("/doLogin") //自定義登錄請求地址 .defaultSuccessUrl("/hello") //登錄無權限訪問頁面默認調整頁面 .usernameParameter("loginName") .passwordParameter("loginPassword") .permitAll()//不攔截 .and() .csrf()//記得關閉 .disable(); } }
原理
1.在登錄時發現提交參數增加了一個remember-me: on
2.退出瀏覽器都會默認帶上cookie
3.將remember-me的值通過base 64解密發現
值為: liqiang:1610094382046:5e86d9769643e32fef52583ae468a52a
第一個為用戶名 第二個為過期時間 第三個為加密key
4.remomber-me生成源碼
org.springframework.security.web.authentication.rememberme.TokenBasedRememberMeServices
public void onLoginSuccess(HttpServletRequest request, HttpServletResponse response, Authentication successfulAuthentication) { //從登錄Authentication 獲取用戶名和密碼 String username = this.retrieveUserName(successfulAuthentication); String password = this.retrievePassword(successfulAuthentication); if (!StringUtils.hasLength(username)) { this.logger.debug("Unable to retrieve username"); } else { //因為登錄成功密碼有可能被其他filter擦除 這里如果沒有再查一次 if (!StringUtils.hasLength(password)) { UserDetails user = this.getUserDetailsService().loadUserByUsername(username); password = user.getPassword(); if (!StringUtils.hasLength(password)) { this.logger.debug("Unable to obtain password for user: " + username); return; } } //獲取過期時間秒默認是 應該是可配的 默認是1209600 int tokenLifetime = this.calculateLoginLifetime(request, successfulAuthentication); long expiryTime = System.currentTimeMillis(); //當前時間加上過期時間毫秒 expiryTime += 1000L * (long)(tokenLifetime < 0 ? 1209600 : tokenLifetime); //算出加密值 String signatureValue = this.makeTokenSignature(expiryTime, username, password); // 用戶名+:+過期時間+:密碼+:+加密值 寫入cookie 並設置過期時間 this.setCookie(new String[]{username, Long.toString(expiryTime), signatureValue}, tokenLifetime, request, response); if (this.logger.isDebugEnabled()) { this.logger.debug("Added remember-me cookie for user '" + username + "', expiry: '" + new Date(expiryTime) + "'"); } } } protected String makeTokenSignature(long tokenExpiryTime, String username, String password) { //用戶名+:+過期時間+:密碼+:秘鑰 這個getKey是uuid 每次重啟 都需要重新登錄 我們可以配置寫死
String data = username + ":" + tokenExpiryTime + ":" + password + ":" + this.getKey(); MessageDigest digest; try { //進行md5加密 digest = MessageDigest.getInstance("MD5"); } catch (NoSuchAlgorithmException var8) { throw new IllegalStateException("No MD5 algorithm available!"); } return new String(Hex.encode(digest.digest(data.getBytes()))); }
key配置
protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests() .anyRequest().authenticated() .and().rememberMe() .key("system") .and() .formLogin() // .loginPage("/login.html")//登錄頁面路徑 // .loginProcessingUrl("/doLogin") .usernameParameter("loginName") .passwordParameter("loginPassword") .defaultSuccessUrl("/hello") .failureForwardUrl("/loginFail") .failureUrl("/login.html") .permitAll()//不攔截 .and() //替換默認的登錄處理器 注意順序 因為下面有給攔截器的屬性賦值 比如 usernameParameter passwordParameter .addFilterAt(initLoginFilter(), UsernamePasswordAuthenticationFilter.class) .csrf()//記得關閉 .disable(); }
5.校驗
org.springframework.security.web.authentication.rememberme.RememberMeAuthenticationFilter
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest)req; HttpServletResponse response = (HttpServletResponse)res; if (SecurityContextHolder.getContext().getAuthentication() == null) { //先調用rememberMeServices 的autoLogin 取出cookie 進行解析校驗有消息 Authentication rememberMeAuth = this.rememberMeServices.autoLogin(request, response); if (rememberMeAuth != null) { try { rememberMeAuth = this.authenticationManager.authenticate(rememberMeAuth); SecurityContextHolder.getContext().setAuthentication(rememberMeAuth); this.onSuccessfulAuthentication(request, response, rememberMeAuth); if (this.logger.isDebugEnabled()) { this.logger.debug("SecurityContextHolder populated with remember-me token: '" + SecurityContextHolder.getContext().getAuthentication() + "'"); } if (this.eventPublisher != null) { this.eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(SecurityContextHolder.getContext().getAuthentication(), this.getClass())); } if (this.successHandler != null) { this.successHandler.onAuthenticationSuccess(request, response, rememberMeAuth); return; } } catch (AuthenticationException var8) { if (this.logger.isDebugEnabled()) { this.logger.debug("SecurityContextHolder not populated with remember-me token, as AuthenticationManager rejected Authentication returned by RememberMeServices: '" + rememberMeAuth + "'; invalidating remember-me token", var8); } this.rememberMeServices.loginFail(request, response); this.onUnsuccessfulAuthentication(request, response, var8); } } chain.doFilter(request, response); } else { if (this.logger.isDebugEnabled()) { this.logger.debug("SecurityContextHolder not populated with remember-me token, as it already contained: '" + SecurityContextHolder.getContext().getAuthentication() + "'"); } chain.doFilter(request, response); } }
持久化令牌
簡單使用
配合記住登錄使用
可以通過redis和數據庫存儲token
- 避免使用內存session,一旦用戶量大了,內存極有可能爆掉。
- 避免使用內存session,重啟后需要重新登錄。
- 分布式情況下應用存儲 別的地方獲取不到token
默認實現有2種 InMemoryTokenRepositoryImpl 內存 或者JdbcTokenRepositoryImpl數據庫,表結構可以參考impl里面的crud 手動創建
參考的org.springframework.security.web.authentication.rememberme.JdbcTokenRepositoryImpl 實現 由查詢數據庫改為查redis
@Component public class RedisPersistentTokenRepository implements PersistentTokenRepository { TokenRedisRepository tokenRedisRepository; PersistentRememberMeTokenConvert persistentRememberMeTokenConvert; @Override public void createNewToken(PersistentRememberMeToken persistentRememberMeToken) { PersistentRememberMeTokenRo persistentRememberMeTokenRo=persistentRememberMeTokenConvert.toRo(persistentRememberMeToken); //保存到redis hash方式 key為token:{series} tokenRedisRepository.save(persistentRememberMeToken); } @Override public void updateToken(String series, String tokenValue, Date lastUsed) { PersistentRememberMeTokenRo persistentRememberMeTokenRo=tokenRedisRepository.findOne(series); persistentRememberMeTokenRo.setDate(lastUsed); persistentRememberMeTokenRo.setTokenValue(tokenValue); //保存到redis hash方式 key為token:{userName} tokenRedisRepository.save(persistentRememberMeToken); } @Override public PersistentRememberMeToken getTokenForSeries(String series) { PersistentRememberMeTokenRo persistentRememberMeTokenRo=tokenRedisRepository.findOne(series); return persistentRememberMeTokenConvert.toPersistentRememberMeToken(persistentRememberMeTokenRo);; } @Override public void removeUserTokens(String series) { tokenRedisRepository.del(s); } }
配置
http.authorizeRequests() .anyRequest().authenticated() .and() .rememberMe() .tokenRepository(getApplicationContext().getBean(RedisPersistentTokenRepository.class)) .key("system") .and() .formLogin() // .loginPage("/login.html")//登錄頁面路徑 // .loginProcessingUrl("/doLogin") .usernameParameter("loginName") .passwordParameter("loginPassword") .defaultSuccessUrl("/hello") .failureForwardUrl("/loginFail") .failureUrl("/login.html") .permitAll()//不攔截 .and() //替換默認的登錄處理器 注意順序 因為下面有給攔截器的屬性賦值 比如 usernameParameter passwordParameter .addFilterAt(initLoginFilter(), UsernamePasswordAuthenticationFilter.class) .csrf()//記得關閉 .disable();
源碼
1.寫入
org.springframework.security.web.authentication.rememberme.PersistentTokenBasedRememberMeServices
protected void onLoginSuccess(HttpServletRequest request, HttpServletResponse response, Authentication successfulAuthentication) { String username = successfulAuthentication.getName(); this.logger.debug("Creating new persistent login for user " + username); //創建persistentTokend對象 PersistentRememberMeToken persistentToken = new PersistentRememberMeToken(username, this.generateSeriesData(), this.generateTokenData(), new Date()); try { //持久化 this.tokenRepository.createNewToken(persistentToken); //寫入cookie this.addCookie(persistentToken, request, response); } catch (Exception var7) { this.logger.error("Failed to save persistent token ", var7); } }
2.校驗
重寫了父類的此方法
org.springframework.security.web.authentication.rememberme.PersistentTokenBasedRememberMeServices#processAutoLoginCookie