Spring Security系列——1.登录相关
Posted 结构化思维wz
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Spring Security系列——1.登录相关相关的知识,希望对你有一定的参考价值。
Spring Security(1.登录相关)
登录
1.spring security初体验
- 创建项目勾选依赖
- 只要写一个接口,就会默认被保护起来(访问接口时,必须先登录,用户名是user 密码是自动生成的)
自定义用户名,密码
-
配置文件版:
spring.security.user.name=wangze spring.security.user.password=123456 #配置角色 spring.security.user.roles=admin
-
java配置
spring5之后,密码一定要加密,此例中提供一个过期的方案(告诉系统不加密)
@Configuration public class SecurityConfig extends WebSecurityConfigurerAdapter{ //告诉系统密码不加密 @Bean PasswordEncoder passwordEncoder() { return NoOpPasswordEncoder.getInstance(); } @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.inMemoryAuthentication() //基于内存的认证 .withUser("wangze") .password("123456").roles("admin"); //and(). }
为什么用 and 相连呢?
在没有 Spring Boot 的时候,我们都是 SSM 中使用 Spring Security,这种时候都是在 XML 文件中配置 Spring Security,既然是 XML 文件,标签就有开始有结束,现在的 and 符号相当于就是 XML 标签的结束符,表示结束当前标签,这是个时候上下文会回到 inMemoryAuthentication 方法中,然后开启新用户的配置。
2.自定义表单登录
自己的项目要有自己的登录页面,springsecurity提供的登录页面不满足我们的需求时候,我们可以选择自定义!
-
首先,导入你的登录界面到static中
-
在security配置类中配置
@Override //配置http相关 protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests() //开启配置 .anyRequest().authenticated() //所有请求认证之后才可访问 .and() .formLogin() //表单登录 .loginProcessingUrl("/doLogin") //处理登录请求的地址 .loginPage("/login.html") //配置页 .permitAll() //与登录请求相关的都给过 .and().csrf().disable();//关闭csrf供给策略方便测试 }
web.ignoring() 用来配置忽略掉的 URL 地址,一般对于静态文件,我们可以采用此操作。 如果我们使用 XML 来配置 Spring Security ,里边会有一个重要的标签 <http>,HttpSecurity 提供的配置方法 都对应了该标签。 authorizeRequests 对应了 <intercept-url>。 formLogin 对应了 <formlogin>。 and 方法表示结束当前标签,上下文回到HttpSecurity,开启新一轮的配置。 permitAll 表示登录相关的页面/接口不要被拦截。
-
虽然配置了登录页,但是此时我们的css与js还是会被拦截,所以我们要放行静态资源
@Override public void configure(WebSecurity web) throws Exception { web.ignoring().antMatchers("/js/**","/css/**","/image/**"); }
细节分析:
1.登录接口:
登录接口对应表单中的 action :
在表单中写的接口,如果不在配置类中配置登录接口,那么登录页面跟登录接口是一样的(login.html) 。登录接口post ,登录页面是get 。如果用.loginProcessingUrl("/doLogin") 配置之后,登录接口就是(/dologin)
2.登录参数
表单与服务器端参数对应表单中的name
默认是:
<form action="/login.html" method="post">
<input type="text" name="username" id="name">
<input type="password" name="password" id="pass">
<button type="submit">
<span>登录</span>
</button>
</form>
如果我们想修改username:
.usernameParameter("name")
3.登录回调(前后端不分离的情况)
登录成功回调
在 Spring Security 中,和登录成功重定向 URL 相关的方法有两个:
- defaultSuccessUrl
- successForwardUrl
这两个咋看没什么区别,实际上内藏乾坤。
首先我们在配置的时候,defaultSuccessUrl 和 successForwardUrl 只需要配置一个即可,具体配置哪个,则要看你的需求,两个的区别如下:
- defaultSuccessUrl 有一个重载的方法,我们先说一个参数的 defaultSuccessUrl 方法。如果我们在 defaultSuccessUrl 中指定登录成功的跳转页面为
/index
,此时分两种情况,如果你是直接在浏览器中输入的登录地址,登录成功后,就直接跳转到/index
,如果你是在浏览器中输入了其他地址,例如http://localhost:8080/hello
,结果因为没有登录,又重定向到登录页面,此时登录成功后,就不会来到/index
,而是来到/hello
页面。 - defaultSuccessUrl 还有一个重载的方法,第二个参数如果不设置默认为 false,也就是我们上面的的情况,如果手动设置第二个参数为 true,则 defaultSuccessUrl 的效果和 successForwardUrl 一致。
- successForwardUrl 表示不管你是从哪里来的,登录后一律跳转到 successForwardUrl 指定的地址。例如 successForwardUrl 指定的地址为
/index
,你在浏览器地址栏输入http://localhost:8080/hello
,结果因为没有登录,重定向到登录页面,当你登录成功之后,就会服务端跳转到/index
页面;或者你直接就在浏览器输入了登录页面地址,登录成功后也是来到/index
。
相关配置如下:
.and()
.formLogin()
.loginPage("/login.html")
.loginProcessingUrl("/doLogin")
.usernameParameter("name")
.passwordParameter("passwd")
.defaultSuccessUrl("/index")
.successForwardUrl("/index")
.permitAll()
.and()
注意:实际操作中,defaultSuccessUrl 和 successForwardUrl 只需要配置一个即可。
2.登录失败回调
与登录成功相似,登录失败也是有两个方法:
- failureForwardUrl
- failureUrl
这两个方法在设置的时候也是设置一个即可。failureForwardUrl 是登录失败之后会发生服务端跳转,failureUrl 则在登录失败之后,会发生重定向。
4.注销登录
注销登录的默认接口是 /logout
,我们也可以配置。
.and()
.logout()
.logoutUrl("/logout")
.logoutRequestMatcher(new AntPathRequestMatcher("/logout","POST"))
.logoutSuccessUrl("/index")
.deleteCookies()
.clearAuthentication(true)
.invalidateHttpSession(true)
.permitAll()
.and()
注销登录的配置我来说一下:
- 默认注销的 URL 是
/logout
,是一个 GET 请求,我们可以通过 logoutUrl 方法来修改默认的注销 URL。 - logoutRequestMatcher 方法不仅可以修改注销 URL,还可以修改请求方式,实际项目中,这个方法和 logoutUrl 任意设置一个即可。
- logoutSuccessUrl 表示注销成功后要跳转的页面。
- deleteCookies 用来清除 cookie。
- clearAuthentication 和 invalidateHttpSession 分别表示清除认证信息和使 HttpSession 失效,默认可以不用配置,默认就会清除。
3.http配置
我们的需求不可能是对所有请求的拦截,所以我们需要对不同url配置不同的拦截策略!
配置http使不同角色访问权限不同
@GetMapping("/admin/hello")
public String admin(){
return "hello admin!!";
}
@GetMapping("/user/hello")
public String user(){
return "hello user!!";
}
配置类:
package com.example.security.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
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.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
/**
* @author: 王泽
*/
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
//告诉系统密码不加密,spring5以后密码必须加密
@Bean
PasswordEncoder passwordEncoder() {
return NoOpPasswordEncoder.getInstance();
}
@Override //配置用户相关
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication() //基于内存的认证
.withUser("王泽").password("123456").roles("user");
}
@Override //配置http相关
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests() //开启配置
.antMatchers("/admin/**") //ant风格的匹配符
.hasRole("admin")//具备某个角色
.antMatchers("/user/**").hasAnyRole("admin","user")
.anyRequest().authenticated() //除了上述两个只要登录就能访问
.and()
.formLogin() //表单登录
.loginProcessingUrl("/doLogin") //处理登录请求的地址
.permitAll() //与登录请求相关的都给过
.and().csrf().disable(); //关机csrf供给策略方便测试
}
}
现在我们有两个用户: admin的wangze ;user的"王泽"; 接下来测试登录用户王泽的时候是否能访问admin/hello:
使用wangze来登录admin/hello:
4.前后端分离的开发方式
不再返回页面,已JSON的形式来交互。
1. 无状态登录
1.1 什么是有状态
有状态服务,即服务端需要记录每次会话的客户端信息,从而识别客户端身份,根据用户身份进行请求的处理,典型的设计如 Tomcat 中的 Session。例如登录:用户登录后,我们把用户的信息保存在服务端 session 中,并且给用户一个 cookie 值,记录对应的 session,然后下次请求,用户携带 cookie 值来(这一步有浏览器自动完成),我们就能识别到对应 session,从而找到用户的信息。这种方式目前来看最方便,但是也有一些缺陷,如下:
- 服务端保存大量数据,增加服务端压力
- 服务端保存用户状态,不支持集群化部署
1.2 什么是无状态
微服务集群中的每个服务,对外提供的都使用 RESTful 风格的接口。而 RESTful 风格的一个最重要的规范就是:服务的无状态性,即:
- 服务端不保存任何客户端请求者信息
- 客户端的每次请求必须具备自描述信息,通过这些信息识别客户端身份
那么这种无状态性有哪些好处呢?
- 客户端请求不依赖服务端的信息,多次请求不需要必须访问到同一台服务器
- 服务端的集群和状态对客户端透明
- 服务端可以任意的迁移和伸缩(可以方便的进行集群化部署)
- 减小服务端存储压力
1.3 如何实现无状态
无状态登录的流程:
- 首先客户端发送账户名/密码到服务端进行认证
- 认证通过后,服务端将用户信息加密并且编码成一个 token,返回给客户端
- 以后客户端每次发送请求,都需要携带认证的 token
- 服务端对客户端发送来的 token 进行解密,判断是否有效,并且获取用户登录信息
1.4 各自优缺点
使用 session 最大的优点在于方便。你不用做过多的处理,一切都是默认的即可。
但是使用 session 有另外一个致命的问题就是如果你的前端是 android、ios、小程序等,这些 App 天然的就没有 cookie,如果非要用 session,就需要这些工程师在各自的设备上做适配,一般是模拟 cookie,从这个角度来说,在移动 App 遍地开花的今天,我们单纯的依赖 session 来做安全管理,似乎也不是特别理想。
这个时候 JWT 这样的无状态登录就展示出自己的优势了,这些登录方式所依赖的 token 你可以通过普通参数传递,也可以通过请求头传递,怎么样都行,具有很强的灵活性。
不过话说回来,如果你的前后端分离只是网页+服务端,其实没必要上无状态登录,基于 session 来做就可以了,省事又方便。
2.JSON交互
2.1 前后端分离的数据交互
在前后端分离这样的开发架构下,前后端的交互都是通过 JSON 来进行,无论登录成功还是失败,都不会有什么服务端跳转或者客户端跳转之类。
登录成功了,服务端就返回一段登录成功的提示 JSON 给前端,前端收到之后,该跳转该展示,由前端自己决定,就和后端没有关系了。
登录失败了,服务端就返回一段登录失败的提示 JSON 给前端,前端收到之后,该跳转该展示,由前端自己决定,也和后端没有关系了。
首先把这样的思路确定了,基于这样的思路,我们来看一下登录配置。
2.2 登录成功
之前我们配置登录成功的处理是通过如下两个方法来配置的:
- defaultSuccessUrl
- successForwardUrl
这两个都是配置跳转地址的,适用于前后端不分的开发。除了这两个方法之外,还有一个必杀技,那就是 successHandler。
successHandler 的功能十分强大,甚至已经囊括了 defaultSuccessUrl 和 successForwardUrl 的功能。我们来看一下:
.successHandler((req, resp, authentication) -> {
Object principal = authentication.getPrincipal();
resp.setContentType("application/json;charset=utf-8");
PrintWriter out = resp.getWriter();
out.write(new ObjectMapper().writeValueAsString(principal));
out.flush();
out.close();
})
successHandler 方法的参数是一个 AuthenticationSuccessHandler 对象,这个对象中我们要实现的方法是 onAuthenticationSuccess。
onAuthenticationSuccess 方法有三个参数,分别是:
- HttpServletRequest
- HttpServletResponse
- Authentication
有了前两个参数,我们就可以在这里随心所欲的返回数据了。利用 HttpServletRequest 我们可以做服务端跳转,利用 HttpServletResponse 我们可以做客户端跳转,当然,也可以返回 JSON 数据。
第三个 Authentication 参数则保存了我们刚刚登录成功的用户信息。
测试:
登录成功:
2.3登陆失败
.failureHandler((req, resp, e) -> {
resp.setContentType("application/json;charset=utf-8");
PrintWriter out = resp.getWriter();
out.write(e.getMessage());
out.flush();
out.close();
})
失败的回调也是三个参数,前两个就不用说了,第三个是一个 Exception,对于登录失败,会有不同的原因,Exception 中则保存了登录失败的原因,我们可以将之通过 JSON 返回到前端。
我们还可以精确识别异常类型:
resp.setContentType("application/json;charset=utf-8");
PrintWriter out = resp.getWriter();
RespBean respBean = RespBean.error(e.getMessage());
if (e instanceof LockedException) {
respBean.setMsg("账户被锁定,请联系管理员!");
} else if (e instanceof CredentialsExpiredException) {
respBean.setMsg("密码过期,请联系管理员!");
} else if (e instanceof AccountExpiredException) {
respBean.setMsg("账户过期,请联系管理员!");
} else if (e instanceof DisabledException) {
respBean.setMsg("账户被禁用,请联系管理员!");
} else if (e instanceof BadCredentialsException) {
respBean.setMsg("用户名或者密码输入错误,请重新输入!");
}
out.write(new ObjectMapper().writeValueAsString(respBean));
out.flush();
out.close();
3.未认证登录
那未认证又怎么办呢?
有小伙伴说,那还不简单,没有认证就访问数据,直接重定向到登录页面就行了,这没错,系统默认的行为也是这样。
但是在前后端分离中,这个逻辑明显是有问题的,如果用户没有登录就访问一个需要认证后才能访问的页面,这个时候,我们不应该让用户重定向到登录页面,而是给用户一个尚未登录的提示,前端收到提示之后,再自行决定页面跳转。
在 Spring Security 的配置中加上自定义的 AuthenticationEntryPoint
处理方法,该方法中直接返回相应的 JSON 提示即可。这样,如果用户再去直接访问一个需要认证之后才可以访问的请求,就不会发生重定向操作了,服务端会直接给浏览器一个 JSON 提示,浏览器收到 JSON 之后,该干嘛干嘛。
.exceptionHandling()
.authenticationEntryPoint((req, resp, authException) -> {
resp.setContentType("application/json;charset=utf-8");
PrintWriter out = resp.getWriter();
out.write("尚未登录,请先登录");
out.flush();
out.close();
}
);
4.注销登录
注销登录我们前面说过,按照前面的配置,注销登录之后,系统自动跳转到登录页面,这也是不合适的,如果是前后端分离项目,注销登录成功后返回 JSON 即可,配置如下:
.and()
.logout()
.logoutUrl("/logout")
.logoutSuccessHandler((req, resp, authentication) -> {
resp.setContentType("application/json;charset=utf-8");
PrintWriter out = resp.getWriter();
out.write("注销成功");
out.flush();
out.close();
})
这样,注销成功之后,前端收到的也是 JSON 了
以上是关于Spring Security系列——1.登录相关的主要内容,如果未能解决你的问题,请参考以下文章
spring boot系列--spring security (基于数据库)登录和权限控制
spring boot系列--spring security (基于数据库)登录和权限控制
spring boot系列03--spring security (基于数据库)登录和权限控制(下)