SpringSecurity原理剖析及其实战(三)

SpringSecurity原理剖析及其实战(三)

    • 1、自定义登录页面
    • 2、过滤器链模式与责任链模式
    • 3、Security Session
    • 4、RememberMe实现
    • 5、退出登录
    • 6、CSRF

1、自定义登录页面

默认登录页面通过DefaultLoginPageGeneratingFilter#generateLoginPageHtml生成

  • 编写登录页面
默认登录页面通过DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Titletitle>
head>
<body>
<form action="/user/login" method="post">
    用户名:<input type="text" name="username"/><br/>
    密码: <input type="password" name="password"/><br/>
    <input type="submit" value="提交"/>
form>
body>
html>
  • 自定义登录页面配置
 @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.formLogin() //表单提交
                .loginPage("/login.html") //自定义登录页面 
                .loginProcessingUrl("/user/login") //登录访问路径,必须和表单提交接口一样 
                .defaultSuccessUrl("/admin/index") //认证成功之后跳转的路径 
                .and().authorizeRequests()//设置哪些路径可以直接访问,不需要认证 
                .antMatchers("/user/login","/login.html")
                .permitAll() .anyRequest().authenticated() //需要认证
                .and().csrf().disable(); //关闭csrf防护
        
    }

测试
访问/admin/demo直接返回结果,不用认证
SpringSecurity原理剖析及其实战(三)_第1张图片
访问/admin/index跳转到自定义登录界面
SpringSecurity原理剖析及其实战(三)_第2张图片
常见问题:
SpringSecurity原理剖析及其实战(三)_第3张图片
原因是登录只接受Post请求 如下,通过successForwardUrl和failureForwardUrl设置登录成功和失败后的跳转页面

@Override
    protected void configure(HttpSecurity http) throws Exception {
        // 认证
        http.formLogin() //表单登录
          .loginPage("/login.html") //自定义登录
          .loginProcessingUrl("/user/login")
          .successForwardUrl("/main") //认证成功之后转发的路径,必须是Post请求
          .failureForwardUrl("/toerror") //认证失败之后转发的路径,必须是Post请求
          .and().authorizeRequests()
          // 授权指定的请求,不用认证
          .antMatchers("/user/login","/login.html","/error.html").permitAll()
          .anyRequest().authenticated()//需要认证
          .and().csrf().disable(); //关闭csrf防护
    }

controller.java

@Controller
public class LoginController {

    @RequestMapping("/main")
    public String main() {
        return "redirect:/main.html";
    }

    @RequestMapping("/toerror")
    public String error() {
        return "redirect:/error.html";
    }

}

自定义用户名和密码参数名
当进行登录时会执行 UsernamePasswordAuthenticationFilter 过滤器

  • usernamePasrameter :账户参数名
  • passwordParameter :密码参数名
  • postOnly=true :默认情况下只允许POST请求

SpringSecurity原理剖析及其实战(三)_第4张图片

2、过滤器链模式与责任链模式

  • 2.1 过滤器链模式与责任链模式的区别

    • 2.1.1 过滤器链模式

过滤器链模式又称标准模式,这种模式主要使用不同标准来过滤一组对象。
过滤的过程便是一个层层筛选的过程,因此过滤器模式属于结构型设计模式的一种。

    • 2.1.2 责任链模式

责任链模式顾名思义就是创建一个链条,经过这个链条处理的所有对象和数据分别进行依次加工,每个环节负责处理不同的业务,环节间彼此解耦,同时可以复用。

    • 场景说明

通信行业如移动会有很多营销活动,而这些营销活动的对象是有要求的,有的需要判断在网时长,有的需要有最低套餐要求等;
1.中国移动客户是目标角色;
2.它不同营销活动的要求是过滤器角色;
3.登录认证服务器需要经历层层过滤校验

    • 区别

过滤器链模式属于结构型设计模式
责任链模式属于行为型设计模式
结构型模式的目的是通过组合类或对象产生更大结构以适应更高层次的逻辑需求
从实现代码过滤上来看,过滤器模式更像是为了分组group by,而责任链模式是让多个对象都有可能接受请求,将这些对象连接成一条链,并且沿着这条链传递请求。

  • 2.2 AbstractAuthenticationProcessingFilter的认证过滤
    SpringSecurity原理剖析及其实战(三)_第5张图片

3、Security Session

  • 3.1 会话管理 (Session)

用户认证通过后,为了避免用户的每次操作都进行认证可将用户的信息保存在会话中。spring
security提供会话管理,认证通过后将身份信息放入Security
ContextHolder上下文,SecurityContext与当前线程进行绑定,方便获取用户身份。

  • 3.1.1 cookie 和session的区别
    • 简介

这里做一个小复习,我们知道客户端请求服务器端是用http(超文本传输协议)请求,而http请求是无状态的,所以我们的状态是存在服务器(session)或者浏览器(cookie)

    • 区别

1.cookie数据存放在客户的浏览器上,session数据放在服务区上
2.cookie不是很安全,别人可以分析存放在本地的COOKIE并进行COOKIE欺骗考虑到安全应当使用session。
3.设置cookie时间可以使cookie过期。但是使用session-destory(),我们将会销毁会话。
4.session会在一定时间内保存在服务器上。当访问增多,会比较占用你服务器的性能考虑到减轻服务器性能方面,应当使用cookie。
5.单个cookie保存到数据不能超过4k,很多浏览器都限制一个站点最多保存20个cookie。(session对象没有对存储的数据量的限制,其中可以保存更为复杂的数据类型)

    • 注意:

1、session很容易失效,用户体验很差;
2、虽然cookie不安全,但是可以加密 ;
3、cookie也分为永久和暂时存在的;
4、浏览器 有禁止cookie功能 ,但一般用户都不会设置;
5、一定要设置失效时间,要不然浏览器关闭就消失了;

SpringSecurity原理剖析及其实战(三)_第6张图片

    • 3.2 获取用户身份信息
@RestController
@RequestMapping("/admin")
public class AdminController {

    @GetMapping("/demo")
    public String demo() {
        String username = getUsername();
        return username + "spring security demo";
    }

    private String getUsername() {
        //获取当前登录的用户信息
        Authentication authentication = SecurityContextHolder.getContext()
                .getAuthentication();
        if (!authentication.isAuthenticated()) {
            return null;
        }
        Object principal = authentication.getPrincipal();
        String username = null;
        if(principal instanceof UserDetails) {
            username = ((UserDetails)principal).getUsername();
        } else {
            username = principal.toString();
        }
        return username;
    }

}
    • 3.3 会话控制
      我们可以通过以下选项准确控制会话何时创建以及Spring Security如何与之交互:
机制 描述
always 如果session不存在总是需要创建
ifRequired 如果需要就创建一个session(默认)登录时
never Spring Security 将不会创建session,但是如果应用中其他地方创建了session,那 么Spring Security将会使用它
stateless Spring Security将绝对不会创建session,也不使用session。并且它会暗示不使用 cookie,所以每个请求都需要重新进行身份验证。这种无状态架构适用于REST API 及其无状态认证机制。
http.sessionManagement()
                .invalidSessionUrl("/session/invalid");

默认情况下,Spring Security会为每个登录成功的用户会新建一个Session,就是ifRequired 。在执行认 证过程之前,spring security将运行SecurityContextPersistenceFilter过滤器负责存储安全请求上下 文,上下文根据策略进行存储,默认为HttpSessionSecurityContextRepository ,其使用http session 作为存储器。
SpringSecurity原理剖析及其实战(三)_第7张图片

    • 会话超时

可以在sevlet容器中设置Session的超时时间,如下设置Session有效期为600s; spring boot 配置文件:
SpringSecurity原理剖析及其实战(三)_第8张图片

注意:session最低60s,参考源码TomcatServletWebServerFactory#configureSession:
SpringSecurity原理剖析及其实战(三)_第9张图片

session超时之后,可以通过Spring Security 设置跳转的路径。

/**
 * @author shengwencheng
 * @sice 2021-11-05 10:36
 */
@RestController
@RequestMapping("/session")
public class SessionController {

    @GetMapping("/invalid")
    @ResponseStatus(code = HttpStatus.UNAUTHORIZED)
    public String sessionInvalid() {
        return "session失效";
    }
}
    • 会话并发控制

用户在这个手机登录后,他又在另一个手机登录相同账户,对于之前登录的账户 是否需要被挤兑,或者说在第二次登录时限制它登录,更或者像腾讯视频VIP账号一样,最多只能五个人同时登录,第六个人限制登录。

      • maximumSession:最大会话数量,设置为1表示一个用户只能有一个会话
      • expiredSessionStrategy:会话过期策略
http.sessionManagement()
                .invalidSessionUrl("/session/invalid")
                .maximumSessions(1)
                .expiredSessionStrategy(new MyExpiredSessionStrategy());
public class MyExpiredSessionStrategy implements
SessionInformationExpiredStrategy {
@Override
    public void onExpiredSessionDetected(SessionInformationExpiredEvent event)
throws IOException, ServletException {
} }
HttpServletResponse response = event.getResponse(); response.setContentType("application/json;charset=UTF-8"); response.getWriter().write("您已被挤兑下线!");

测试

  1. 使用chrome浏览器,先登录,再访问http://localhost:8080/admin/index
  2. 使用ie浏览器,再登录,再访问http://localhost:8080/admin/index
  3. 使用chrome浏览器,重新访问http://localhost:8080/admin/index,会执行
    expiredSessionStrategy,页面上显示 “您已被挤兑下线!”

阻止用户第二次登录
sessionManagement也可以配置 maxSessionsPreventsLogin:boolean值,当达到 maximumSessions设置的最大会话个数时阻止登录。

http.sessionManagement()
                .invalidSessionUrl("/session/invalid")
                .maximumSessions(1)
                .expiredSessionStrategy(new MyExpiredSessionStrategy())
                .maxSessionsPreventsLogin(true);
    • 3.4 集群session

SpringSecurity原理剖析及其实战(三)_第10张图片

实际场景中一个服务会至少有两台服务器在提供服务,在服务器前面会有一个nginx做负载均衡,用户访 问nginx,nginx再决定去访问哪一台服务器。当一台服务宕机了之后,另一台服务器也可以继续提供服 务,保证服务不中断。如果我们将session保存在Web容器(比如tomcat)中,如果一个用户第一次访问被 分配到服务器1上面需要登录,当某些访问突然被分配到服务器二上,因为服务器二上没有用户在服务器 一上登录的会话session信息,服务器二还会再次让用户登录,用户已经登录了还让登录就感觉不正常 了。解决这个问题的思路是用户登录的会话信息不能再保存到Web服务器中,而是保存到一个单独的库 (redis、mongodb、mysql等)中,所有服务器都访问同一个库,都从同一个库来获取用户的session信 息,如用户在服务器一上登录,将会话信息保存到库中,用户的下次请求被分配到服务器二,服务器二 从库中检查session是否已经存在,如果存在就不用再登录了,可以直接访问服务了。

    • 引入spring session依赖
<dependency>
    <groupId>org.springframework.session</groupId>
    <artifactId>spring-session-data-redis</artifactId>
</dependency>
<dependency>
    <groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
    <version>3.1.0</version>
</dependency>

修改application.yaml

spring:
  session:
    store-type: redis
  redis:
    host: localhost
    port: 6379
server:
  port: 8080
  servlet:
    session:
      timeout: 600

测试
启动两个服务8080,8081 ,其中一个登录后访问http://localhost:8080/admin/index,另外一个不需
要登录就可以访问

安全会话cookie 我们可以使用httpOnly和secure标签来保护我们的会话cookie:

  • httpOnly:如果为true,那么浏览器脚本将无法访问cookie
  • secure:如果为true,则cookie将仅通过HTTPS连接发送

spring boot配置文件:

server.servlet.session.cookie.http-only=true server.servlet.session.cookie.secure=true

4、RememberMe实现

Spring Security 中 Remember Me 为“记住我”功能,用户只需要在登录时添加 remember-me复选框,
取值为true。Spring Security 会自动把用户信息存储到数据源中,以后就可以不登录进行访问。

    • RememberMe配置
@Autowired
public DataSource dataSource;


@Override
protected void configure(HttpSecurity http) throws Exception {
//记住我
         http.rememberMe()
        .tokenRepository(persistentTokenRepository())//设置持久化仓库
        .tokenValiditySeconds(3600) //超时时间,单位s 默认两周
        .userDetailsService(userServiceImpl); //设置自定义登录逻辑

    }

    public PersistentTokenRepository persistentTokenRepository(){ JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl(); //设置数据源
        jdbcTokenRepository.setDataSource(dataSource);
        return jdbcTokenRepository;
    }


    • 创建数据库
create table persistent_logins (
    username varchar(64) not null,
    series varchar(64) primary key,
    token varchar(64) not null,
    last_used timestamp not null
)

在客户端登录页面中添加 remember-me 的复选框,只要用户勾选了复选框下次就不需要进行登录了。

<form action="/user/login" method="post">
    用户名:<input type="text" name="username"/><br/>
    密码: <input type="password" name="password"/><br/>
    <input type="checkbox" name="remember-me" value="true"/><br/>
    <input type="submit" value="提交"/>
form>

在这里插入图片描述

5、退出登录

Spring security默认实现了logout退出,用户只需要向 Spring Security 项目中发送 /logout 退出请求即 可。
默认的退出 url 为 /logout ,退出成功后跳转到 /login?logout 。
SpringSecurity原理剖析及其实战(三)_第11张图片
自定义退出逻辑
如果不希望使用默认值,可以通过下面的方法进行修改。

http.logout()
.logoutUrl("/logout")
.logoutSuccessUrl("/login.html");

SecurityContextLogoutHandler
当退出操作出发时,将发生:
销毁HTTPSession 对象 清除认证状态
跳转到 /login.html
SpringSecurity原理剖析及其实战(三)_第12张图片
LogoutSuccessHandler
退出成功处理器,实现 LogoutSuccessHandler 接口 ,可以自定义退出成功处理逻辑。

6、CSRF

    • 简介

1、CSRF(Cross-site request forgery)跨站请求伪造,也被称为“OneClick Attack” 或者Session Riding。 通过伪造用户请求访问受信任站点的非法请求访问。

2、跨域:只要网络协议,ip 地址,端口中任何一个不相同就是跨域请求。

3、客户端与服务进行交互时,由于 http 协议本身是无状态协议,所以引入了cookie进行记录客户端身份。在cookie中会存放session id用来识别客户端身份的。在跨域的情况下,session id 可能被第三方恶意劫 持,通过这个session id 向服务端发起请求时,服务端会认为这个请求是合法的,可能发生很多意想不 到的事情。

防御CSRF攻击策略

    • 验证 HTTP Referer 字段
    • 在请求地址中添加 token 并验证
    • 在 HTTP 头中自定义属性并验证

从 Spring Security4开始CSRF防护默认开启,默认会拦截请求,进行CSRF处理。CSRF为了保证不是其 他第三方网站访问,要求访问时携带参数名为 _csrf 值为token(token 在服务端产生,在渲染请求页面时 埋入页面)的内容,如果token和服务端的token匹配成功,则正常访问。

修改login.html

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    <form action="/login" method="post">
        <input type="hidden" th:value="${_csrf.token}" name="_csrf"
th:if="${_csrf}"/>
用户名:<input type="text" name="username"/><br/> 密码: <input type="password" name="password"/><br/> <input type="submit" value="提交"/>
    </form>
</body>
</html>

修改配置类

//关闭csrf防护
// http.csrf().disable();

以上就是Spring Security的认证使用,下篇是认证服务器整体流程的梳理,喜欢的朋友点个关注,顺便推荐一个不错的Spring Cloud Alibaba + vue的项目,https://gitee.com/youlaitech/youlai-mall

-> SpringSecurity原理剖析及其实战(四)

你可能感兴趣的:(Spring,Security,Java,后端,java,后端)