1.记住我
- 【回顾:什么是cookie】在网站中,http请求是无状态的,也就是说,即使第一次和服务器连接后并且登录成功后,第二次请求服务器依然不能知道当前请求是哪个用户;cookie的出现就是为了解决这个问题:当浏览器访问网站后,这些网站将一组数据存放在客户端,当该用户发送第二次请求的时候,就会自动的把上次请求存储的cookie数据自动携带给服务器,服务器通过浏览器携带的数据就能识别当前用户
- 现在很多的网站登陆的时候都有一个记住我功能,实现的原理其实就是学习javaweb的时候学习的cookie,只要勾选的记住我,服务器就会回应客户端一个cookie,只要在cookie过期之前,再次请求同一个页面,不用输入账号+密码,由于有cookie证明了你的身份,所以你就可以通过"认证"过程
- spring security对于"记住我"功能也做了支持,和跳转登录页、注销功能一样,只需要去调用方法http.rememberMe(),即可开启认证登录页的"记住我"功能
- 测试
- 关闭浏览器,再次请求localhost:8080
- 分析原理
- 当我们手动将这个本地cookie清理掉之后,我们就需要重新进行身份"认证"的环节
测试完成!
2.定制登录页
-
因为我们调用方法http.formLogin()开启了用户未登录状态就请求资源将被重定向到登陆页面的功能,但是跳转的登陆页面是spring security自带的一个登陆页面,这样显然不是我们想要的,我们想要的必然是身份验证要走我们自己写的更加好看的登陆页
-
怎么定制自己的登录页呢?查看http.formLogin()的源码
-
通过源码我们可以发现,这个方法在父类中的实现果然还是没有参考价值,有参考价值的还是方法上的注释
* Specifies to support form based authentication. If * {@link FormLoginConfigurer#loginPage(String)} is not specified a default login page * will be generated. * 支持用户指定基于表单的身份验证,就是支持我们自己指定一个表单页面来进行身份认证 * 如果用户没有指定,将使用spring security自带的身份认证表单页面 * The configuration below demonstrates customizing the defaults. * 下面的配置演示如何自定义默认值 * * * @Configuration * @EnableWebSecurity * public class FormLoginSecurityConfig extends WebSecurityConfigurerAdapter { * * @Override * protected void configure(HttpSecurity http) throws Exception { * http.authorizeRequests().antMatchers("/**").hasRole("USER").and().formLogin() * .usernameParameter("username") // default is username * .passwordParameter("password") // default is password * //get方式请求的默认login页面,我们就可以通过这个方法指定我们自己的登录页为身份认证页面 * //注意:loginPage()中指定的是这个页面的请求url,不是资源真实地址 * .loginPage("/authentication/login") // default is /login with an HTTP get * .failureUrl("/authentication/login?failed") // default is /login?error * .loginProcessingUrl("/authentication/login/process"); // default is /login * // with an HTTP * // post * }
-
指定我们自己的登录页为spring security的身份认证页面
http.formLogin().loginPage("/toLogin");
-
现在我们指定了spring security的身份认证页面了,那么页面应该把数据提交到哪里才能实现身份认证呢?
-
【解决】看http.formLogin()的源码,看看原原来默认的身份认证页面的action属性是什么,直接拷贝过来使用
public FormLoginConfigurer<HttpSecurity> formLogin() throws Exception { return getOrApply(new FormLoginConfigurer<>()); }
-
可见这个方法只是new了一个对象FormLoginConfigurer,我们去查看这个类的源码;注意:下面的源码只是节选了我们自己需要的部分
public final class FormLoginConfigurer<H extends HttpSecurityBuilder<H>>...{ //构造 public FormLoginConfigurer() { super(new UsernamePasswordAuthenticationFilter(), null); usernameParameter("username"); passwordParameter("password"); } /** * <p> * Specifies the URL to send users to if login is required. If used with * {@link WebSecurityConfigurerAdapter} a default login page will be generated when * this attribute is not specified. * </p> * * <p> * If a URL is specified or this is not being used in conjuction with * {@link WebSecurityConfigurerAdapter}, users are required to process the specified * URL to generate a login page. In general, the login page should create a form that * submits a request with the following requirements to work with * {@link UsernamePasswordAuthenticationFilter}: * </p> * * <ul> * <li>It must be an HTTP POST</li> * <li>It must be submitted to {@link #loginProcessingUrl(String)}</li> * <li>It should include the username as an HTTP parameter by the name of * {@link #usernameParameter(String)}</li> * <li>It should include the password as an HTTP parameter by the name of * {@link #passwordParameter(String)}</li> * </ul> * * <h2>Example login.jsp</h2> * * Login pages can be rendered with any technology you choose so long as the rules * above are followed. Below is an example login.jsp that can be used as a quick start * when using JSP's or as a baseline to translate into another view technology. * * <pre> * <!-- loginProcessingUrl should correspond to FormLoginConfigurer#loginProcessingUrl. Don't forget to perform a POST --> * <c:url value="/login" var="loginProcessingUrl"/> * <form action="${loginProcessingUrl}" method="post"> * <fieldset> * <legend>Please Login</legend> * <!-- use param.error assuming FormLoginConfigurer#failureUrl contains the query parameter error --> * <c:if test="${param.error != null}"> * <div> * Failed to login. * <c:if test="${SPRING_SECURITY_LAST_EXCEPTION != null}"> * Reason: <c:out value="${SPRING_SECURITY_LAST_EXCEPTION.message}" /> * </c:if> * </div> * </c:if> * <!-- the configured LogoutConfigurer#logoutSuccessUrl is /login?logout and contains the query param logout --> * <c:if test="${param.logout != null}"> * <div> * You have been logged out. * </div> * </c:if> * <p> * <label for="username">Username</label> * <input type="text" id="username" name="username"/> * </p> * <p> * <label for="password">Password</label> * <input type="password" id="password" name="password"/> * </p> * <!-- if using RememberMeConfigurer make sure remember-me matches RememberMeConfigurer#rememberMeParameter --> * <p> * <label for="remember-me">Remember Me?</label> * <input type="checkbox" id="remember-me" name="remember-me"/> * </p> * <div> * <button type="submit" class="btn">Log in</button> * </div> * </fieldset> * </form> * </pre> * * <h2>Impact on other defaults</h2> * * Updating this value, also impacts a number of other default values. For example, * the following are the default values when only formLogin() was specified. * * <ul> * <li>/login GET - the login form</li> * <li>/login POST - process the credentials and if valid authenticate the user</li> * <li>/login?error GET - redirect here for failed authentication attempts</li> * <li>/login?logout GET - redirect here after successfully logging out</li> * </ul> * * If "/authenticate" was passed to this method it update the defaults as shown below: * * <ul> * <li>/authenticate GET - the login form</li> * <li>/authenticate POST - process the credentials and if valid authenticate the user * </li> * <li>/authenticate?error GET - redirect here for failed authentication attempts</li> * <li>/authenticate?logout GET - redirect here after successfully logging out</li> * </ul> * * * @param loginPage the login page to redirect to if authentication is required (i.e. * "/login") * @return the {@link FormLoginConfigurer} for additional customization */ @Override public FormLoginConfigurer<H> loginPage(String loginPage) { return super.loginPage(loginPage); } }
-
上面的源码中我只选出了FormLoginConfigurer的构造和一个成员方法loginPage(),loginPage()显然就是配置spring security的login页面的,但是这个方法本身实现没有什么好注意的,值得注意的是它上面的注释,它上面的注释大概的意思就是说如果我们在设置spring security的登录页的时候没有显式的配置一个我们自己的login page,那么spring security将会自动为我们生成一个登陆表单页面,并且它把这个表单的代码也粘贴在了注释里,但是由于编码问题,html页面的表单的< >两个尖括号变成了& lt ;,这就导致我们阅读起来比较有障碍,但是我们还是可以从注释中找到表单的定义,或者我们可以使用markdowm编辑器查看,这样就会还原表单的定义
-
注释中说明的自动生成的表单视图中表单的定义为:
<form action="${loginProcessingUrl}" method="post">
-
从表单定义中我们可以发现,表单提交方式为POST,提交的地址为
${loginProcessingUrl}
,所以我们可以照猫画虎,将我们自定义的登录页的表单提交地址改成这个地址,这样加上我们在config中自定义了spring security的登录页,我们就可以从自定义的登录页提交用户名+密码给spring security完成我们的身份"认证"<form th:action="${loginProcessingUrl}" method="post">
- 测试
- 从上面的结果来看,我们设置的表单提交地址完全正确
- 搞完表单地址,我们来说说传递的用户名和密码后端怎么接收的,这里就需要看看FormLoginConfigurer类的构造了,在上面粘贴源码的时候我特意把它也粘贴了出来
public FormLoginConfigurer() { super(new UsernamePasswordAuthenticationFilter(), null); usernameParameter("username"); passwordParameter("password"); }
- 注意:从上面的代码来看,应该就是后端接收前端传递的用户名+密码的时候是按照username+password两个名称来进行接收的,我们可以去自定义的表单页看看我们的form表单中的两个input标签的name属性
- 可见,账号+密码命名与后端接收参数的名称一致,所以spring security才能接收到我们在表单中填写的账号+密码;但是光是嘴上说肯定不行,我们需要来检测我们的结论是否正确
- 检测办法:把前端input标签的name属性改了,看看我们提交的用户数据能不能通过spring security的"认证"
- 再次登陆测试
- 那么我们前端的input的name属性就只能是username+password?当然不是,spring security将变量的命名权力都交给了我们,我们只需要调用对应的方法即可设置变量名
- 方法是什么?看formLogin()的源码
* @Override * protected void configure(HttpSecurity http) throws Exception { * http.authorizeRequests().antMatchers("/**").hasRole("USER").and().formLogin() * .usernameParameter("username") // default is username * .passwordParameter("password") // default is password * .loginPage("/authentication/login") // default is /login with an HTTP get * .failureUrl("/authentication/login?failed") // default is /login?error * .loginProcessingUrl("/authentication/login/process"); // default is /login * // with an HTTP * // post * }
- 设置接收的变量名与前端一致
http.formLogin().loginPage("/toLogin").usernameParameter("uname").passwordParameter("pwd");
- 再次测试登陆
测试成功!
3.解决定制登录页之后,注销功能失灵的情况
- 在上面定制了登陆页面之后,我们点击注销的时候发现此时注销功能失效了
- 为什么会这样呢?看loginPage()的源码
* <h2>Impact on other defaults</h2> * * Updating this value, also impacts a number of other default values. For example, * the following are the default values when only formLogin() was specified. * * <ul> * <li>/login GET - the login form</li> * <li>/login POST - process the credentials and if valid authenticate the user</li> * <li>/login?error GET - redirect here for failed authentication attempts</li> * <li>/login?logout GET - redirect here after successfully logging out</li> * </ul> * * If "/authenticate" was passed to this method it update the defaults as shown below: * * <ul> * <li>/authenticate GET - the login form</li> * <li>/authenticate POST - process the credentials and if valid authenticate the user * </li> * <li>/authenticate?error GET - redirect here for failed authentication attempts</li> * <li>/authenticate?logout GET - redirect here after successfully logging out</li> * </ul> *
- 上面的源码中的注释说的意思大概就是如果我们自己设置了spring security的登陆页面,那么spring security的"登录页(get和post两种)的url将会改变、登陆错误页(/login?error GET)的url将会改变、注销成功页面(/login?logout GET)的url将会改变"
- 假设我们新定义的登录页的url为"/toLogin",那么上面改变的值改变之后的值为"/toLogin"(get和post请求登录页)、"/toLogin?error"(登陆错误页)、"/toLogin?logout"注销成功页
- 注意它上面一直说的都是注销成功之后的跳转的页面的url会被修改,那么我们注销的url会不会也修改呢?猜测:①注销url"logout"也会修改 ②注销url"logout"不会修改;验证两种猜测哪一个正确的办法就是再次测试注销功能,然后再去请求localhost:8080,看看用户是不是还登陆上的
- 验证上面的推论
- 可见猜测1正确
- 分析原因:对比没有开启定制登录页的时候我们是怎么注销的,抓一下注销的时候的包
- 我们来看看确认注销页面到底做了什么
- 到这里我们可以大致的明白我们加上定制登录页之后注销的时候发生了什么变化了,在没有定制登录页的时候,我们向logout请求的时候使用的是post请求,并且提交的时候提交了隐藏域参数,这才使得我们成功注销已经登陆的spring security账户
- 当我们定制了登陆页面之后,我们前端点击注销按钮向后端发起的是一个GET请求,这里就出现了区别了,我们可以通过查看 http.logout().logoutUrl()的源码来查看为什么GET请求不能实现注销功能
/** * The URL that triggers log out to occur (default is "/logout"). If CSRF protection * is enabled (default), then the request must also be a POST. This means that by * default POST "/logout" is required to trigger a log out. If CSRF protection is * disabled, then any HTTP method is allowed. * * <p> * It is considered best practice to use an HTTP POST on any action that changes state * (i.e. log out) to protect against <a * href="https://en.wikipedia.org/wiki/Cross-site_request_forgery">CSRF attacks</a>. If * you really want to use an HTTP GET, you can use * <code>logoutRequestMatcher(new AntPathRequestMatcher(logoutUrl, "GET"));</code> * </p> * * @see #logoutRequestMatcher(RequestMatcher) * @see HttpSecurity#csrf() * * @param logoutUrl the URL that will invoke logout. * @return the {@link LogoutConfigurer} for further customization */ public LogoutConfigurer<H> logoutUrl(String logoutUrl) { this.logoutRequestMatcher = null; this.logoutUrl = logoutUrl; return this; }
- 同样的,重点在注释里
* The URL that triggers log out to occur (default is "/logout"). If CSRF protection * is enabled (default), then the request must also be a POST. This means that by * default POST "/logout" is required to trigger a log out. If CSRF protection is * disabled, then any HTTP method is allowed. * * *触发注销的URL(默认为“/logout”),如果启用了CSRF保护(默认),那么请求也必须是POST * 这意味着默认情况下需要POST“/logout”来触发注销。如果CSRF保护被禁用,则允许任何HTTP方法 * * <p> * It is considered best practice to use an HTTP POST on any action that changes state * (i.e. log out) to protect against <a * href="https://en.wikipedia.org/wiki/Cross-site_request_forgery">CSRF attacks</a>. If * you really want to use an HTTP GET, you can use * <code>logoutRequestMatcher(new AntPathRequestMatcher(logoutUrl, "GET"));</code> * </p> * *最佳做法是在更改状态(即注销)的任何操作上使用HTTP POST来防止 * <a href=“https://en.wikipedia.org/wiki/Cross-site_请求伪造“>CSRF攻击</a> * 如果您真的想使用http get,可以使用 * <code>logoutRequestMatcher(new AntPathRequestMatcher(logoutUrl,“GET”));</code> * * * @see #logoutRequestMatcher(RequestMatcher) * @see HttpSecurity#csrf() * * @param logoutUrl the URL that will invoke logout. * @return the {@link LogoutConfigurer} for further customization *
- 从源码可以得知,为了防止CSRF攻击攻击,注销的时候请求"/logout"的方式必须为POST,如果我们实在想使用GET方式提交请求实现注销功能的话,源码指出了3种实现方法
- ①直接关闭CSRF攻击保护,但是这样显然安全上就有漏洞了
- ②所有请求注销的方式都是由POST
- ③在config类中使用logoutRequestMatcher(new AntPathRequestMatcher(logoutUrl,“GET”))
- 实现方法3
http.logout() //开启注销功能 .logoutSuccessUrl("/") //成功之后跳转的页面 .logoutRequestMatcher(new AntPathRequestMatcher("/logout","GET")); //使得GET方法也可以成功注销用户
- 验证法2:将前端注销a标签改造为表单,且提交数据方式为POST
注销设置还是恢复原来的设置<form th:action="@{/logout}" method="post" style="display: inline-block"> <a class="item"> <input type="submit" value="注销"> </a> </form>
http.logout().logoutSuccessUrl("/");
- 测试法1:直接在config中加一条认证配置
http.csrf().disable();
测试成功!
- 我们对源码中提到的3种方法都进行了测试,可以发现3种方法都可以解决spring security种自定义了登陆页面之后,注销功能不能使用的情况,官方从源码中对于3种方法的使用推荐为:使用POST方式提交 > 配置logoutRequestMatcher(new AntPathRequestMatcher("/logout",“GET”)) > 关闭CSRF攻击保护
4.增加remember Me功能
- 在我们自定义的页面上没有记住我这个选项,这显然不符合我们平时使用的习惯,所以我们应该加上这个功能
- 在使用spring security的登陆页面的时候我们只需要在config类中调用方法http.rememberMe()接口开启该功能,但是在我们自己页面上需要自己手动实现了
- 怎么让spring security知道我点击了我自己定义的一个多选框?看http.rememberMe()的源码
/** * Allows configuring of Remember Me authentication. * * <h2>Example Configuration</h2> * * The following configuration demonstrates how to allow token based remember me * authentication. Upon authenticating if the HTTP parameter named "remember-me" * exists, then the user will be remembered even after their * {@link javax.servlet.http.HttpSession} expires. * * <pre> * @Configuration * @EnableWebSecurity * public class RememberMeSecurityConfig extends WebSecurityConfigurerAdapter { * * @Override * protected void configure(HttpSecurity http) throws Exception { * http * .authorizeRequests(authorizeRequests -> * authorizeRequests * .antMatchers("/**").hasRole("USER") * ) * .formLogin(withDefaults()) * .rememberMe(withDefaults()); * } * } * </pre> * * @param rememberMeCustomizer the {@link Customizer} to provide more options for * the {@link RememberMeConfigurer} * @return the {@link HttpSecurity} for further customizations * @throws Exception */ public HttpSecurity rememberMe(Customizer<RememberMeConfigurer<HttpSecurity>> rememberMeCustomizer) throws Exception { rememberMeCustomizer.customize(getOrApply(new RememberMeConfigurer<>())); return HttpSecurity.this; }
- 这是看源码看到现在,第一个注释没有太大帮助的源码,那么我们可以看一看这个方法的实现,这个方法一看就知道是在配置记住我功能的,方法的实现主要new了一个RememberMeConfigurer对象,我们可以点进去这个类看一看
public final class RememberMeConfigurer{ /** * The default name for remember me parameter name and remember me cookie name */ private static final String DEFAULT_REMEMBER_ME_NAME = "remember-me"; }
- 所以后端接收记住我参数的时候,是按照参数名称为"remember-me"去接收的,所以我们可以将我们多选框中name属性的值修改为"remember-me"
<div class="field"> <input type="checkbox" name="remember-me"> 记住我 </div>
- 测试
测试成功! - 当然,spring security也支持我们自定义remember-me的参数名称,方式为调用http.rememberMe().rememberMeParameter(“自定义的参数名称”),只要我们自定义了,它就会按照我们自定义的参数名称取接收前端表单提交的数据 ,这里就不测试了,就是设置一个参数名称,前端多选框的name属性和它设置相同的属性值即可
5.小结
- 到此spring security就学习的差不多了,在spring security中我们学习了它的两个功能:认证+授权,具体学习的知识点如下:
- 认证:认证用户的角色信息、密码加密
- 授权:登陆、注销、防跨站攻击、记住我、指定资源访问授权
- 直接参考下面的代码来记忆和理解
package com.thhh.config; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.web.util.matcher.AntPathRequestMatcher; @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { //链式编程 @Override protected void configure(HttpSecurity http) throws Exception { //请求授权规则制定,即对于请求服务器上的资源的授权,antMatchers()用于指定哪些资源请求需要进行授权, //permitAll()表示这个资源的请求开放给所有人;hasRole()用于在()中指定将antMatchers()中指定的资源的请求权限开放给指定的人 http.authorizeRequests().antMatchers("/").permitAll() //添加请求验证,对于"/"的访问,运行所有人访问 .antMatchers("/level1/**").hasRole("vip1") //添加请求验证,对于"/level1/**"的访问,只有角色为vip1的角色可以访问 .antMatchers("/level2/**").hasRole("vip2") //同理 .antMatchers("/level3/**").hasRole("vip3"); //同理 http.formLogin().loginPage("/toLogin"); // http.logout().logoutSuccessUrl("/").logoutRequestMatcher(new AntPathRequestMatcher("/logout","GET"));//开启注销功能 http.logout().logoutSuccessUrl("/"); http.csrf().disable(); http.rememberMe();//开启记住我功能 } @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { //AuthenticationManagerBuilder授权管理建造者 auth.inMemoryAuthentication() //inMemoryAuthentication()在内存中存储授权信息 .withUser("admin").password(new BCryptPasswordEncoder().encode("12345")).roles("vip1","vip2","vip3").and() //授权用户1信息配置 .withUser("zhangsan").password(new BCryptPasswordEncoder().encode("12345")).roles("vip1").and() //授权用户2信息配置 .withUser("lisi").password(new BCryptPasswordEncoder().encode("12345")).roles("vip2","vip3").and() //授权用户3信息配置 .passwordEncoder(new BCryptPasswordEncoder()); } }
- 使用spring security实现上面提到的功能比我们自己写过滤器和拦截器快得多,开发效率高
- 有了spring security,对于上面要实现的安全要求,我们直接拿过来使用即可实现