Spring Security系列——1.登录相关

Posted 结构化思维wz

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Spring Security系列——1.登录相关相关的知识,希望对你有一定的参考价值。

Spring Security(1.登录相关)

1.spring security初体验

  • 创建项目勾选依赖
  • 只要写一个接口,就会默认被保护起来(访问接口时,必须先登录,用户名是user 密码是自动生成的)
    在这里插入图片描述

自定义用户名,密码

  1. 配置文件版:

    spring.security.user.name=wangze
    spring.security.user.password=123456
    #配置角色
    spring.security.user.roles=admin
    
  2. 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 只需要配置一个即可,具体配置哪个,则要看你的需求,两个的区别如下:

  1. defaultSuccessUrl 有一个重载的方法,我们先说一个参数的 defaultSuccessUrl 方法。如果我们在 defaultSuccessUrl 中指定登录成功的跳转页面为 /index,此时分两种情况,如果你是直接在浏览器中输入的登录地址,登录成功后,就直接跳转到 /index,如果你是在浏览器中输入了其他地址,例如 http://localhost:8080/hello,结果因为没有登录,又重定向到登录页面,此时登录成功后,就不会来到 /index ,而是来到 /hello 页面。
  2. defaultSuccessUrl 还有一个重载的方法,第二个参数如果不设置默认为 false,也就是我们上面的的情况,如果手动设置第二个参数为 true,则 defaultSuccessUrl 的效果和 successForwardUrl 一致。
  3. 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()

注销登录的配置我来说一下:

  1. 默认注销的 URL 是 /logout,是一个 GET 请求,我们可以通过 logoutUrl 方法来修改默认的注销 URL。
  2. logoutRequestMatcher 方法不仅可以修改注销 URL,还可以修改请求方式,实际项目中,这个方法和 logoutUrl 任意设置一个即可。
  3. logoutSuccessUrl 表示注销成功后要跳转的页面。
  4. deleteCookies 用来清除 cookie。
  5. 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 有另外一个致命的问题就是如果你的前端是 androidios、小程序等,这些 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 (基于数据库)登录和权限控制(下)

Spring Security源码:登录认证源码流程

手把手教你如何使用Spring Security(上):登录授权

爆破专栏丨Spring Security系列教程之实现CAS单点登录上篇-概述