spring-security使用-登录(一)
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
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· AI与.NET技术实操系列(二):开始使用ML.NET
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
· 理解Rust引用及其生命周期标识(上)
· 浏览器原生「磁吸」效果!Anchor Positioning 锚点定位神器解析
· 全程不用写代码,我用AI程序员写了一个飞机大战
· DeepSeek 开源周回顾「GitHub 热点速览」
· 记一次.NET内存居高不下排查解决与启示
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!