@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.httpBasic()//开启httpbasic认证
.and()
.authorizeRequests()
.anyRequest()
.authenticated();//所有请求都需要登录认证才能访问
}
}
启动项目,在项目后台有这样的一串日志打印,冒号后面的就是默认密码。
Using generated security password: 0cc59a43-c2e7-4c21-a38c-0df8d1a6d624
我们可以通过浏览器进行登录验证,默认的用户名是user.(下面的登录框不是我们开发的,是HttpBasic模式自带的)
当然我们也可以通过application.yml指定配置用户名密码
spring:
security:
user:
name: admin
password: admin
所以,HttpBasic模式真的是非常简单又简陋的验证模式,Base64的加密算法是可逆的,你知道上面的原理,分分钟就破解掉。我们完全可以使用PostMan工具,发送Http请求进行登录验证。
Hash算法特别的地方在于它是一种单向算法,用户可以通过hash算法对某个数据生成一段特定长度的唯一hash值,却不能通过这个hash值逆向获取原始数据。因此Hash算法常用在不可还原的密码存储、数据完整性校验等领域。
那问题来了,密码只能单向加密不能解密,那如何校验密码的正确性?我们来看Spring Security中的接口PasswordEncoder ,并对这个问题进行解答。
PasswordEncoder 是Spring Scurity框架内处理密码加密与校验的接口。
public interface PasswordEncoder {
String encode(CharSequence rawPassword);
boolean matches(CharSequence rawPassword, String encodedPassword);
default boolean upgradeEncoding(String encodedPassword) {
return false;
}
}
这个接口有三个方法
例如,我们可以通过如下示例代码在进行用户注册的时候加密存储用户密码
user.setPassword(passwordEncoder.encode(user.getPassword()));
//将User保存到数据库表,该表包含password列
对于upgradeEncoding,例如:如果长期使用一个密码不变,可能存在被破解的风险,建议一段时间可以自动提示需要进行密码的升级操作
BCryptPasswordEncoder 是Spring Security推荐使用的PasswordEncoder接口实现类
public class PasswordEncoderTest {
@Test
void bCryptPasswordTest(){
PasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
String rawPassword = "123456"; //原始密码
String encodedPassword = passwordEncoder.encode(rawPassword); //加密后的密码
System.out.println("原始密码" + rawPassword);
System.out.println("加密之后的hash密码:" + encodedPassword);
System.out.println(rawPassword + "是否匹配" + encodedPassword + ":" //密码校验:true
+ passwordEncoder.matches(rawPassword, encodedPassword));
System.out.println("654321是否匹配" + encodedPassword + ":" //定义一个错误的密码进行校验:false
+ passwordEncoder.matches("654321", encodedPassword));
}
}
上面的测试用例执行的结果是下面这样的。(注意:对于同一个原始密码,每次加密之后的hash密码都是不一样的,这正是BCryptPasswordEncoder的强大之处,它不仅不能被破解,想通过常用密码对照表进行大海捞针你都无从下手
)
原始密码123456
加密之后的hash密码:$2a$10$zt6dUMTjNSyzINTGyiAgluna3mPm7qdgl26vj4tFpsFO6WlK5lXNm
123456是否匹配$2a$10$zt6dUMTjNSyzINTGyiAgluna3mPm7qdgl26vj4tFpsFO6WlK5lXNm:true
654321是否匹配$2a$10$zt6dUMTjNSyzINTGyiAgluna3mPm7qdgl26vj4tFpsFO6WlK5lXNm:false
BCrypt产生随机盐(盐的作用就是每次做出来的菜味道都不一样)。这一点很重要,因为这意味着每次encode将产生不同的结果。
$2a$10$zt6dUMTjNSyzINTGyiAgluna3mPm7qdgl26vj4tFpsFO6WlK5lXNm
BCrypt加密后的密码有三个部分,由 $分隔:
BCrypt*算法生成长度为 60 的字符串,因此我们需要确保密码将存储在可以容纳密码的数据库列中。
Spring Security的HttpBasic模式,该模式比较简单,只是进行了通过携带Http的Header进行简单的登录验证,而且没有可以定制的登录页面,所以使用场景比较窄。
对于一个完整的应用系统,与登录验证相关的页面都是高度定制化的,非常美观而且提供多种登录方式。这就需要Spring Security支持我们自己定制登录页面,也就是本文给大家介绍的formLogin模式登录认证模式。
需要注意的是:有的朋友会被Form Login这个名字误解,Form Login不是只有使用html中的form 表单才能实现登录功能,使用js发起登录请求也是可以的
准备工作
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
把下面的代码从BizpageController里面删掉。这涉及到一个非常重要的问题,就是Spring Security的登录认证并不需要我们自己去写登录认证的Controller方法,而是使用过滤器UsernamePasswordAuthenticationFilter(下一节会源码分析),这个过滤器是默认集成的,所以并不需要我们自己去实现登录认证逻辑。我们实现登录功能只需要做配置就可以了,所以把下面的代码从项目里面删掉。
// 登录
@PostMapping("/login")
public String index(String username,String password) {
return "index";
}
formLogin登录认证模式的三要素:
一般来说,使用权限认证框架的的业务系统登录验证逻辑是固定的,而资源访问控制规则和用户信息是从数据库或其他存储介质灵活加载的。但本文所有的用户、资源、权限信息都是代码配置写死的,旨在为大家介绍formLogin认证模式,如何从数据库加载权限认证相关信息我还会结合RBAC权限模型再写文章的
首先,我们要继承WebSecurityConfigurerAdapter ,重写configure(HttpSecurity http) 方法,该方法用来配置登录验证逻辑。请注意看下文代码中的注释信息。
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable() //禁用跨站csrf攻击防御,后面的章节会专门讲解
.formLogin()
//登录页面
.loginPage("/login.html")//一旦用户的请求没有权限就跳转到这个页面
.loginProcessingUrl("/login")//登录表单form中action的地址,也就是处理认证请求的路径
.usernameParameter("username")///登录表单form中用户名输入框input的name名,不修改的话默认是username
.passwordParameter("password")//form中密码输入框input的name名,不修改的话默认是password
.defaultSuccessUrl("/")//登录认证成功后默认转跳的路径
.and()
.authorizeRequests()
.antMatchers("/login.html","/login").permitAll()//不需要通过登录验证就可以被访问的资源路径
.antMatchers("/","/biz1","/biz2") //资源路径匹配
.hasAnyAuthority("ROLE_user","ROLE_admin") //user角色和admin角色都可以访问
.antMatchers("/syslog","/sysuser") //资源路径匹配
.hasAnyRole("admin") //admin角色可以访问
//.antMatchers("/syslog").hasAuthority("sys:log")
//.antMatchers("/sysuser").hasAuthority("sys:user")
.anyRequest().authenticated();
}
上面的代码分为两部分:
配置资源的访问控制规则
。如:开发登录页面的permitAll开放访问,“/biz1”(业务一页面资源)需要有角色为user或admin的用户才可以访问。hasAnyAuthority("ROLE_user","ROLE_admin")
等价于hasAnyRole("user","admin")
,角色是一种特殊的权限
。"sys:log"
或"sys:user"
是我们自定义的权限ID,有这个ID的用户可以访问对应的资源
这时候我们通过浏览器访问,随便测试一个用户没有访问权限的资源,都会跳转到login.html页面。
在上文中,我们配置了登录验证及资源访问的权限规则,我们还没有具体的用户,下面我们就来配置具体的用户。重写WebSecurityConfigurerAdapter
的 configure(AuthenticationManagerBuilder auth)
方法
@Override
public void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication()
.withUser("user")
.password(passwordEncoder().encode("123456"))
.roles("user")
.and()
.withUser("admin")
.password(passwordEncoder().encode("123456"))
//.authorities("sys:log","sys:user")
.roles("admin")
.and()
.passwordEncoder(passwordEncoder());//配置BCrypt加密
}
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
在我们的实际开发中,登录页面login.html和控制层Controller登录验证’/login’都必须无条件的开放。除此之外,一些静态资源如css、js文件通常也都不需要验证权限,我们需要将它们的访问权限也开放出来。下面就是实现的方法:重写WebSecurityConfigurerAdapter类的configure(WebSecurity web) 方法
@Override
public void configure(WebSecurity web) {
//将项目中静态资源路径开放出来
web.ignoring().antMatchers( "/css/**", "/fonts/**", "/img/**", "/js/**");
}
那么这些静态资源的开放,和Controller服务资源的开放为什么要分开配置?有什么区别呢?
Spring Security的登录验证流程核心就是过滤器链。
SpringSecurity提供了多种登录认证的方式,由多种Filter过滤器来实现,比如:
根据我们不同的需求实现及配置,不同的Filter会被加载到应用中。
如图所示,当用户登陆的时候首先被某一种认证方式的过滤器拦截(以用户名密码登录为例)。如:UsernamePasswordAuthenticationFilter
会使用用户名和密码创建一个登录认证凭证:UsernamePasswordAuthenticationToken,进而获取一个Authentication对象,该对象代表身份验证的主体,贯穿于用户认证流程始终。
随后使用AuthenticationManager 接口对登录认证主体进行authenticate认证。
public interface AuthenticationManager {
Authentication authenticate(Authentication authentication) throwsAuthenticationException;
}
ProviderManager继承于AuthenticationManager是登录验证的核心类。
public class ProviderManager implements AuthenticationManager, MessageSourceAware, InitializingBean {
……
private List<AuthenticationProvider> providers;
……
ProviderManager保管了多个AuthenticationProvider,每一种登录认证方式都可以尝试对登录认证主体进行认证。只要有一种方式被认证成功,Authentication对象就成为被认可的主体。
public interface AuthenticationProvider {
Authentication authenticate(Authentication var1) throws AuthenticationException;
boolean supports(Class<?> var1);
}
public class DaoAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider {
从数据库获取用户信息
所以当我们需要加载用户信息进行登录验证的时候,我们需要实现UserDetailsService
接口,重写loadUserByUsername
方法,参数是用户输入的用户名。返回值是UserDetails
。
完成登录认证之后,将认证完成的Authtication对象(authenticate: true, 有授权列表authority list, 和username信息)放入SecurityContext上下文里面。后续的请求就直接从SecurityContextFilter中获得认证主体,从而访问资源。
我们就以用户名、密码登录方式为例讲解一下Spring Security的登录认证流程。
该过滤器封装用户基本信息(用户名、密码),定义登录表单数据接收相关的信息。如:
UsernamePasswordAuthenticationFilter继承自抽象类AbstractAuthenticationProcessingFilter,该抽象类定义了验证成功与验证失败的处理方法。
AbstractAuthenticationProcessingFilter中定义了验证成功与验证失败的处理Handler。
private AuthenticationSuccessHandler successHandler = new SavedRequestAwareAuthenticationSuccessHandler();
private AuthenticationFailureHandler failureHandler = new SimpleUrlAuthenticationFailureHandler();
也就是说当我们需要自定义验证成功或失败的处理方法时,要去实现AuthenticationSuccessHandler or AuthenticationfailureHandler接口
http.formLogin()
.loginPage("/login.html")//一旦用户的请求没有权限就跳转到这个页面
.defaultSuccessUrl("/")//登录认证成功后默认转跳的路径
.failureUrl("/login.html")//登陆失败跳转路径
默认跳转到defaultSuccessUrl配置的路径对应的资源页面(一般是首页index.html)。
一般也是跳转登录页login.html,重新登录
)。但是在web应用开发过程中需求是千变万化的,有时需要我们针对登录结果做个性化处理,比如:
AuthenticationSuccessHandler
接口是Security
提供的认证成功处理器接口,我们只需要去实现它即可。但是通常来说,我们不会直接去实现AuthenticationSuccessHandler
接口,而是继承SavedRequestAwareAuthenticationSuccessHandler
类,这个类会记住用户上一次请求(上一次登录成功后请求跳转的路径)的资源路径,比如:用户请求books.html
,没有登陆所以被拦截到了登录页,当你完成登陆之后会自动跳转到books.html
,而不是主页面。
@Component
public class MyAuthenticationSuccessHandler
extends SavedRequestAwareAuthenticationSuccessHandler {
//在application配置文件中配置登陆的类型是JSON数据响应还是做页面响应
@Value("${spring.security.logintype}")
private String loginType;
//Jackson JSON数据处理类
private static ObjectMapper objectMapper = new ObjectMapper();
@Override
public void onAuthenticationSuccess(HttpServletRequest request,
HttpServletResponse response,
Authentication authentication)
throws ServletException, IOException {
if (loginType.equalsIgnoreCase("JSON")) {
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write(objectMapper.writeValueAsString(AjaxResponse.success()));
} else {
// 会帮我们跳转到上一次请求的页面上
super.onAuthenticationSuccess(request, response, authentication);
}
}
}
这里我们同样没有直接实现AuthenticationFailureHandler
接口,而是继承SimpleUrlAuthenticationFailureHandler
类。该类中默认实现了登录验证失败的跳转逻辑,即登陆失败之后回到登录页面。我们可以利用这一点简化我们的代码。
@Component
public class MyAuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler {
//在application配置文件中配置登陆的类型是JSON数据响应还是做页面响应
@Value("${spring.security.logintype}")
private String loginType;
private static ObjectMapper objectMapper = new ObjectMapper();
@Override
public void onAuthenticationFailure(HttpServletRequest request,
HttpServletResponse response,
AuthenticationException exception)
throws IOException, ServletException {
if (loginType.equalsIgnoreCase("JSON")) {
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write(
objectMapper.writeValueAsString(
AjaxResponse.error(
new CustomException(
CustomExceptionType.USER_INPUT_ERROR,
"用户名或密码存在错误,请检查后再次登录"))));
} else {
response.setContentType("text/html;charset=UTF-8");
//默认是重定向到失败页面
super.onAuthenticationFailure(request, response, exception);
}
}
}
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Resource
private MyAuthenticationSuccessHandler myAuthenticationSuccessHandler;
@Resource
private MyAuthenticationFailureHandler myAuthenticationFailureHandler;
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable() //禁用跨站csrf攻击防御,后面的章节会专门讲解
.formLogin()
.successHandler(myAuthenticationSuccessHandler)
.failureHandler(myAuthenticationFailureHandler)
//.defaultSuccessUrl("/index")//登录认证成功后默认转跳的路径
//.failureUrl("/login.html") //登录认证是被跳转页面
}
AuthenticationSuccessHandler
和AuthenticationFailureHandler
注入到Spring Security配置类中不要配置defaultSuccessUrl和failureUrl,否则自定义handler将失效。handler配置与URL配置只能二选一
protected final void updateAuthenticationDefaults() {
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");
}
}
最后可以用下面的代码测试一下登录验证的结果。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>首页</title>
<script src="https://cdn.staticfile.org/jquery/1.12.3/jquery.min.js"></script>
</head>
<body>
<h1>字母哥业务系统登录</h1>
<form action="/login" method="post">
<span>用户名称</span><input type="text" name="uname" id="username"/> <br>
<span>用户密码</span><input type="password" name="pword" id="password"/> <br>
<input type="button" onclick="login()" value="登陆">
</form>
<script type="text/javascript">
function login() {
var username = $("#username").val();
var password = $("#password").val();
if (username === "" || password === "") {
alert('用户名或密码不能为空');
return;
}
$.ajax({
type: "POST",
url: "/login",
data: {
"uname": username, //这里的参数名称要和Spring Security配置一致
"pword": password
},
success: function (json) {
if(json.isok){
location.href = '/'; //index.html
}else{
alert(json.message);
location.href = '/login.html'; //index.html
}
},
error: function (e) {
}
});
}
</script>
</body>
</html>
除了登陆成功、登陆失败的结果处理,Spring Security还为我们提供了其他的结果处理类。比如用户未登录就访问系统资源,可以实现AuthenticationEntryPoint 接口进行响应处理,提示用户应该先去登录
public class MyAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest httpServletRequest,
HttpServletResponse httpServletResponse,
AuthenticationException e) throws IOException, ServletException{
//仿造上文使用response将响应信息写回
}
}
比如用户登录后访问没有权限访问的资源,可以实现AccessDeniedHandler 接口进行相应处理,提示用户没有访问权限
public class MyAccessDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest httpServletRequest,
HttpServletResponse httpServletResponse,
AccessDeniedException e) throws IOException, ServletException {
//仿造上文使用response将响应信息写回
}
}
通过下面的方法进行注册生效
protected void configure(HttpSecurity http) throws Exception {
//省略其他配置内容
http.exceptionHandling()
.accessDeniedHandler(accessDeniedHandler)
.authenticationEntryPoint(authenticationEntryPoint);
//省略其他配置内容
}
AuthenticationEntryPoint 用来解决匿名用户访问无权限资源时的异常
AccessDeineHandler 用来解决认证过的用户访问无权限资源时的异常
AuthenticationEntryPoint
AuthenticationEntryPoint 是 Spring Security Web 一个概念模型接口,顾名思义,他所建模的概念是:“认证入口点”。
它在用户请求处理过程中遇到认证异常时,被 ExceptionTranslationFilter 用于开启特定认证方案 (authentication schema) 的认证流程。
该接口只定义了一个方法 :
void commence(HttpServletRequest request, HttpServletResponse response,
AuthenticationException authException) throws IOException, ServletException;
这里参数 request 是遇到了认证异常 authException 用户请求,response 是将要返回给客户的相应,方法 commence 实现,也就是相应的认证方案逻辑会修改 response 并返回给用户引导用户进入认证流程。
当用户请求了一个受保护的资源,但是用户没有通过认证,那么抛出异常,AuthenticationEntryPoint.Commence(…)就会被调用。这个对应的代码在ExceptionTranslationFilter中,如下,当ExceptionTranslationFilter catch到异常后,就会间接调用AuthenticationEntryPoint。
public class ExceptionTranslationFilter extends GenericFilterBean {
private AuthenticationEntryPoint authenticationEntryPoint;
......
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest)req;
HttpServletResponse response = (HttpServletResponse)res;
try {
chain.doFilter(request, response);
this.logger.debug("Chain processed normally");
} catch (IOException var9) {
throw var9;
} catch (Exception var10) {
......
this.handleSpringSecurityException(request, response, chain, (RuntimeException)ase);
}
}
private void handleSpringSecurityException(HttpServletRequest request, HttpServletResponse response, FilterChain chain, RuntimeException exception) throws IOException, ServletException {
......
this.sendStartAuthentication(request, response, chain, (AuthenticationException)exception);
......
}
protected void sendStartAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, AuthenticationException reason) throws ServletException, IOException {
SecurityContextHolder.getContext().setAuthentication((Authentication)null);
this.requestCache.saveRequest(request, response);
this.logger.debug("Calling Authentication entry point.");
this.authenticationEntryPoint.commence(request, response, reason);
}
......
匿名用户访问某个接口时
/** * 认证失败处理类 返回未授权 * 用来解决匿名用户访问无权限资源时的异常 */
@Component
public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint, Serializable {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e)
throws IOException {
response.setCharacterEncoding("utf-8");
response.setContentType("text/javascript;charset=utf-8");
response.getWriter().print(JSONObject.toJSONString(RestMsg.error("没有访问权限!")));
}
}
AccessDeniedHandler
AccessDeniedHandler 仅适用于已通过身份验证的用户。未经身份验证的用户的默认行为是重定向到登录页面(或适用于正在使用的身份验证机制的任何内容)。
已经授权但是没有访问权限
/** * 认证失败处理类 返回未授权 * 用来解决认证过的用户访问无权限资源时的异常 */
@Component
public class CustomAccessDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response,
AccessDeniedException accessDeniedException) throws IOException, ServletException {
response.setCharacterEncoding("utf-8");
response.setContentType("text/javascript;charset=utf-8");
response.getWriter().print(JSONObject.toJSONString(RestMsg.error("没有访问权限!")));
}
}
配置
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private CustomAuthenticationEntryPoint authenticationEntryPoint;
@Autowired
private CustomAccessDeniedHandler customAccessDeniedHandler;
// 省略部分代码
@Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
httpSecurity
// 认证失败处理类
.exceptionHandling()
.authenticationEntryPoint(authenticationEntryPoint)
.accessDeniedHandler(customAccessDeniedHandler);
}
// 省略部分代码
}
Spring Security提供4种方式精确的控制会话的创建:
never
: Spring Security将永远不会主动创建session,但是如果session在当前应用中已经存在,它将使用该sessionstateless
:Spring Security不会创建或使用任何session。适合于接口型的无状态应用(前后端分离无状态应用),这种方式节省内存资源在Spring Security配置中加入session创建的策略。继承WebSecurityConfigurerAdapter ,重写configure(HttpSecurity http) 方法
@Override
protected void configure(HttpSecurity http) throws Exception {
http.sessionManagement()
.sessionCreationPolicy(
SessionCreationPolicy.IF_REQUIRED
)
}
重要的是:该配置只能控制Spring Security如何创建与使用session,而不是控制整个应用程序。如果我们不明确指定,Spring Security可能不会创建session,但是我们的应用程序可能会创建session(一般spring应用的session管理交由Spring Session进行)!
在Spring boot应用中有两种设置会话超时时间的方式,Spring Security对这两种方式完全兼容,即:当会话超时之后用户需要重新登录才能访问应用:
第一种方式是springBoot应用自带的session超时配置,第二种方式是我们使用Spring Session之后,提供的session超时配置。第二种方式的优先级更高。
注意:在Spring Boot中Session超时最短的时间是一分钟,当你的设置小于一分钟的时候,默认为一分钟。
会话超时之后,我们通常希望应用跳转到一个指定的URL,显示会话超时信息。可以使用如下的配置的代码实现。
http.sessionManagement()
.invalidSessionUrl("/invalidSession.html"); //非法超时session跳转页面
以上路径需要配置permitAll()权限,即无需授权即可访问。
session-fixation-protection 即session的固化保护功能,该功能的目的是一定程度上防止非法用户窃取用户session及cookies信息,进而模拟session的行为。
//默认配置
http.sessionManagement().sessionFixation().migrateSession()
如果这不是您需要的方式,则可以使用其他的选项:
migrateSession
- 即对于同一个cookies的SESSIONID用户,每次登录访问之后访问将创建一个新的HTTP Session会话,旧的HTTP Session会话将无效,并且旧Session会话的属性将被复制。在Servlet 3.0及其之前的版本,这种方式是默认的changeSessionId
- 这种方式不会创建新的session,作为替代,使用Servlet 容器(HttpServletRequest#changeSessionId())提供的会话固化保护功能 。这个选项在Servlet 3.1 (Java EE 7) 或者更新版本的web容器下默认生效。 每次登录访问之后都更换sessionid
,但是没有新建session会话。熟悉Session实现原理的朋友一定都知道,提高Cookies的安全性,实际上就是提高session的安全性。在Spring Boot中可以通过配置方式来实现:
server.servlet.session.cookie.http-only=true
server.servlet.session.cookie.secure=true
虽然固化保护的策略可以一定程度保护session复制、窃取,但是在我们绝大部分的应用需求中,都会限制一个用户只能占用一个session。就像我们经常使用QQ,用户在别的地方登录,之前的登陆就会下线。使用Spring Security的配置我们可以轻松的实现这个功能。
.sessionManagement()
.maximumSessions(1)
.maxSessionsPreventsLogin(false)
.expiredSessionStrategy(new CustomExpiredSessionStrategy())
通过实现SessionInformationExpiredStrategy 接口来自定义session被下线(超时)之后的处理策略。可以跳转到某一个url对应的HTML页面上,这个页面给用户相对有好的提示:您的登录认证已经过期,或者您在另外的设备上进行登录,这里被迫下线。
public class CustomExpiredSessionStrategy implements SessionInformationExpiredStrategy {
//页面跳转的处理逻辑
private RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();
@Override
public void onExpiredSessionDetected(SessionInformationExpiredEvent event) throws IOException, ServletException {
// 是跳转html页面,url代表跳转的地址
redirectStrategy.sendRedirect(event.getRequest(), event.getResponse(), "某个url");
}
}
如果你开发的是前后端分离的应用,使用JSON进行数据交互,可以使用如下代码。
public class CustomExpiredSessionStrategy implements SessionInformationExpiredStrategy {
//jackson的JSON处理对象
private ObjectMapper objectMapper = new ObjectMapper();
@Override
public void onExpiredSessionDetected(SessionInformationExpiredEvent event) throws IOException {
Map<String, Object> map = new HashMap<>();
map.put("code", 403);
map.put("msg", "您的登录已经超时或者已经在另一台机器登录,您被迫下线。"
+ event.getSessionInformation().getLastRequest());
// Map -> Json
String json = objectMapper.writeValueAsString(map);
//输出JSON信息的数据
event.getResponse().setContentType("application/json;charset=UTF-8");
event.getResponse().getWriter().write(json);
}
}
Session(超详细)
做web开发,怎么能不懂cookie、session和token呢?
版权说明 : 本文为转载文章, 版权归原作者所有 版权申明
原文链接 : https://blog.csdn.net/m0_53157173/article/details/121387553
内容来源于网络,如有侵权,请联系作者删除!