Spring Security学习

232 阅读19分钟

Spring Security应用及其源码分析

spring security入门

spring security简介

spring security是一个功能强度且高度可定制的身份验证和访问控制框架。它是用于保护基于spring的应用程序的实际标准。spring security是一个框架,致力于为Java应用程序提供身份验证和授权。与所有spring项目一样,spring security的真正强大之处在于可以轻松扩展以满足自定义要求。

image.png

安全技术方案对比

目前在整个Java开发的系统中,需要用于身份验证和访问控制框架的框架除了spring security,还有一个就是Apache shiro框架

  1. Shrio shiro是一个强大而灵活的开源安全框架,能够非常清晰的处理认知、授权、管理以及密码加密。如下是它具有的特点

    • 易于理解的Java Security API;
    • 简单的身份认知(登录),支持多种数据源(LDAP、JBC、Kerberos、ActiveDirectory等);
    • 对角色的简单的鉴权(访问控制),支持细粒度的鉴权;
    • 支持一级缓存,以提升应用程序的性能;
    • 内置的基于POJO企业会话管理,适用于Web以及非Web的环境;
    • 异构客户端会话访问;
    • 非常简单的加密API
    • 不限任何的框架或者容器捆绑,可以独立运行
  2. Spring Security **除了不能脱离Spring,shiro的功能它都有。而且spring security对Oauth、OpenID也有支持,Shiro则需要自己手动实现,Spring Security的权限细粒度更高。

     OAuth在“客户端”与“服务提供商”之间,设置了一个授权层(authorization layer)。“客户端”不能直接登录“服务器提供商”,只能登录授权层,以此讲用户与客户端分开来。“客户端”登录授权层所用的令牌(token),与用户的密码不同,用户可以在登录的时候,指定授权层令牌的权限范围和有效期。
     “客户端”登录授权层以后,“服务提供商”根据令牌的权限范围和有效期,向“客户端”开放用户储存的资料。
     
     
     
     
     OpenID系统的第一部分是身份验证,即如何通过uri来认证身份。目前的网站都是依靠用户名和密码来登录认证,这就意味着大家在每个网站都需要注册用户名和密码,即便你使用的是同样的密码。如果OpenId,你的网站地址(URI)就是你的用户名,而你的密码存储在一个OpenID服务网站上(你可以自己建立一个OpenID服务网站,也可以选择一个可信任的OpenID服务网站来完成注册)。
     与OpenID同属性的身份识别服务商还要VIeID,ClaimID,CarSpace,Rapleaf,Trufina ID Card等,其中VIe通用账户的应用最为广泛
     
    

spring security框架功能简介

  1. 认证:用户登录:解决的问题是“你是谁?”
  2. 授权:判断用户拥有什么权限,可以访问什么资源,解决的是“你能干什么?”
  3. 安全防护,防止跨站请求,session攻击

spring security应用场景

1.用户登录

image.png

2.用户授权,在系统中用户拥有哪些操作权限

image.png

3.单一登录,一个账号只能在同一时间只能在一个地方登录,如果在其他地方进行二次登录,则剔除之前的登录操作

image.png

4.集成cas,做单点登录,即多个系统只需要登录一次,无需重复登录

5.继承OAuth2,做登录授权,可以用于app登录和第三方登录(qq,vx)等。也可以实现cas的功能。

spring security入门案例

  1. 导入坐标

image.png

  1. 编写controller

image.png

  1. 访问controller会进入security的界面,username为user,密码在idea控制台给出

image.png

Spring Security认证

Sprin security认证基本原理与认证2种方式

如果使用Spring Security框架,该框架会默认自动地替换我们讲系统中的资源进行保护,每次访问资源的时候必须经过一层身份校验,如果通过了则重定向到我们输入的url中,否则访问是被拒绝的。那么Spring Security框架是如何实现的呢?**Spring Security功能的实现主要是由一系列过滤器相互配合完成,也称之为过略器链

过略器链介绍

image.png

过略器是一种典型的AOP思想,下面简单了解下这些过略器链,后续再源码刨析中涉及到过略器链在仔细讲解

image.png

image.png

image.png

表单认证

/**
 * Spring Security 配置类
 */
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {


    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        super.configure(auth);
    }

    @Override
    public void configure(WebSecurity web) throws Exception {
        //解决静态资源被拦截
        web.ignoring().antMatchers("/css/**","/images/js** ");
    }

    /**
     * http请求
     * @param http
     * @throws Exception
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.formLogin() //表单认证
                .loginPage("xxx.html")
                .and()
                .authorizeHttpRequests()
                .antMatchers("xxx.html")
                .permitAll()//放行登录界面
                .anyRequest()
                .authenticated();//所有的请求都需要认证
    }



}

Spring Security中,安全构建器HttpSecurity和WebSecurity的区别是:

  1. WebSecurity不仅通过HttpSecurity定义某些请求的安全控制,也通过其他方式定义其他某些请求可以忽略安全控制;
  2. HttSecurity仅用于定义需要安全控制的请求(当然HttpSecurity也可以指定某些请求不需要安全控制)
  3. 可以认为HTTPSecurityWebSecurity的一部分,WebSecurity是包含HttpSecurity的更大的一个概念;
  4. 构建目标不同
    • WebSecurity构建目标是整个Spring Security安全过略器FilterChainProxy;
    • HttpSecurity的构建目标仅仅是FilterChainProxy中的一个SecurityFilterChain

表单登录

通过过略器链中我们指定有个过略器UsernamePasswordAuthenticationFilter是处理表单登录的。那么下面我们来通过源码观察下这个过略器。

image.png

从源码中可以观察到,表单中的input的name值是username和password,并且表单提交的路径为/login,表单提交方式method为post,这些可以修改为自定义的值

代码如下:

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.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;

/**
 * Spring Security 配置类
 */
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {


    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        super.configure(auth);
    }

    @Override
    public void configure(WebSecurity web) throws Exception {
        //解决静态资源被拦截
        web.ignoring().antMatchers("/css/**","/images/js** ");
    }

    /**
     * http请求
     * @param http
     * @throws Exception
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.formLogin() //表单认证
                .loginPage("xxx.html") //自定义登录页面
                .loginProcessingUrl("/xxx") //表单提交的路径
                .usernameParameter("username")
                .usernameParameter("password") //自定义input的name值
                .successForwardUrl("/") //登录成功之后跳转的路径
                .and()
                .authorizeHttpRequests()
                .antMatchers("xxx.html")
                .permitAll()//放行登录界面
                .anyRequest()
                .authenticated();//所有的请求都需要认证

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

        // 加载同源域名下iframe页面
        http.headers().frameOptions().sameOrigin();
    }



}

基于数据库实现认证功能

之前我们所使用的用户名和密码是来源于框架自动生成的那么我们如何实现基于数据库中的用户名和密码功能呢?要实现security的一个UserDetailsService接口,重写这个接口里面loadUserByUserName即可

  • 编写MyUserDetailsService并实现UserDetailsService接口,重写loadUserByUsername方法
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;

import java.util.ArrayList;


/**
 * 基于数据库完成认证
 */
@Service
public class MyUserDetailsService implements UserDetailsService {

    /**
     * 根据用户名查询用户
     * @param username 前台传入的用户名
     * @return
     * @throws UsernameNotFoundException
     */

    @Autowired
    private UserSerice userSerice;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        User user = userSerice.findByUserName(username);
        if (user == null) {
//            return null;
            throw new UsernameNotFoundException("用户没有找到," + username);
        }

        UserDetails userDetails = new org.springframework.security.core.userdetails.User(
                username,
                user.getPassword(),
                true, //用户是否启用
                true,// 用户是否过期
                true, //用户凭证是否过期
                true, //用户是否锁定
                new ArrayList<>()
        );
        return null;
    }
}
  • 配置类
import com.lagou.service.MyUserDetailsService;
import org.springframework.beans.factory.annotation.Autowired;
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.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;

/**
* Spring Security 配置类
*/
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

   @Autowired
   MyUserDetailsService myUserDetailsService;

   /**
    * 身份安全管理器
    * @param auth
    * @throws Exception
    */
   @Override
   protected void configure(AuthenticationManagerBuilder auth) throws Exception {
       auth.userDetailsService(myUserDetailsService);
   }

   @Override
   public void configure(WebSecurity web) throws Exception {
       //解决静态资源被拦截
       web.ignoring().antMatchers("/css/**","/images/js** ");
   }

   /**
    * http请求
    * @param http
    * @throws Exception
    */
   @Override
   protected void configure(HttpSecurity http) throws Exception {
       http.formLogin() //表单认证
               .loginPage("xxx.html") //自定义登录页面
               .loginProcessingUrl("/xxx") //表单提交的路径
               .usernameParameter("username")
               .usernameParameter("password") //自定义input的name值
               .successForwardUrl("/") //登录成功之后跳转的路径
               .and()
               .authorizeHttpRequests()
               .antMatchers("xxx.html")
               .permitAll()//放行登录界面
               .anyRequest()
               .authenticated();//所有的请求都需要认证

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

       // 加载同源域名下iframe页面
       http.headers().frameOptions().sameOrigin();
   }



}

密码加密认证

在基于数据库完成用户登录的过程中,我们所使用的密码是明文的,规则是通过对密码明文添加{noop}前缀。那么下面对Spring Security中的密码编码进行一些探讨。

Spring Security中PasswordEncoder就是我们对密码进行编码的工具接口。该接口只有两个功能:一个是匹配验证。另一个是密码编码

image.png

image.png

获取当前登录用户

在传统web系统中,我们将登录成功的用户放入session中,在需要的时候就可以从session获取用户,那么Spring Security中我们如何获取当前已登录的用户呢?

  • SecurityContextHolder

    保留系统当前安全上下文的SecurityContext,其中就包括当前使用系统的用户信息

  • SecurityContext 安全上下文,获取当前经过身份验证的主体或身份验证请求令牌

代码实现:

 ```
import org.springframework.security.core.Authentication;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller("/user")
public class UserController {


/**
 * 获取当前登录用户
 */
@GetMapping("/loginUser")
@ResponseBody
public UserDetails getCurrentUser() {
    UserDetails principal = (UserDetails) SecurityContextHolder.getContext().getAuthentication();
    return principal;
}


@GetMapping("/loginUser2")
@ResponseBody
public UserDetails getCurrentUser(Authentication authentication) {
    Object principal = authentication.getPrincipal();
    return (UserDetails) principal;
}

@GetMapping("/loginUser3")
@ResponseBody
public UserDetails getCurrentUser3(@AuthenticationPrincipal UserDetails userDetails) {
    return userDetails;
}

}

remember me记住我

在大多数网站中,都会实现RememberMe这个功能,方便用户下一次登录时直接登录,避免再次输入用户名以及密码去登录,Spring Security针对这个功能已经帮助我们实现,下面来看它的原理图

  • 简单的Token生成方法

    image.png Token=MD5(username+分隔符+expiryTime+分隔符+password)

    注意:这种方式不推荐使用,有严重的安全问题,就是密码信息在千吨啊浏览器cookie中存放,如果cookie被盗取就很容易破解

代码实现:

  1. 前端页面需要增加remember-me的复选框 image.png 2. 修改配置类
import com.lagou.service.MyUserDetailsService;
import org.springframework.beans.factory.annotation.Autowired;
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.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;

/**
 * Spring Security 配置类
 */
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    MyUserDetailsService myUserDetailsService;

    /**
     * 身份安全管理器
     * @param auth
     * @throws Exception
     */
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(myUserDetailsService);
    }

    @Override
    public void configure(WebSecurity web) throws Exception {
        //解决静态资源被拦截
        web.ignoring().antMatchers("/css/**","/images/js** ");
    }

    /**
     * http请求
     * @param http
     * @throws Exception
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.formLogin() //表单认证
                .loginPage("xxx.html") //自定义登录页面
                .loginProcessingUrl("/xxx") //表单提交的路径
                .usernameParameter("username")
                .usernameParameter("password") //自定义input的name值
                .successForwardUrl("/") //登录成功之后跳转的路径
                .and().rememberMe()//开启记住我功能
                .tokenValiditySeconds(1209600) //token失效时间 默认是两周
                .rememberMeParameter("remember-me") //自定义表单input值
                .and()
                .authorizeHttpRequests()
                .antMatchers("xxx.html")
                .permitAll()//放行登录界面
                .anyRequest()
                .authenticated();//所有的请求都需要认证

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

        // 加载同源域名下iframe页面
        http.headers().frameOptions().sameOrigin();
    }



}
  • 持久化的Token生成方法

image.png

存入数据库Token包含:

token:随机生成策略,每次访问都会重新生成 series:登录序列号,随机生成策略。用户生成用户名和密码时,该值重新生成。使用remember-me功能, 该值保持不变 expiryTime:token过期时间。 CookieValue=encode(series+token)

代码实现:

  1. 后台代码
@Bean 
public PersistentTokenRepository tokenRepository() { 
    JdbcTokenRepositoryImpl jdbcTokenRepositoryImpl =new JdbcTokenRepositoryImpl(); 
    jdbcTokenRepositoryImpl.setDataSource(dataSource); //自动创建数据库表:persistent_logins,使用一次后注释掉,不然会报错 
        //jdbcTokenRepositoryImpl.setCreateTableOnStartup(true); 
    return jdbcTokenRepositoryImpl; 
}
/**
 * http请求
 * @param http
 * @throws Exception
 */
@Override
protected void configure(HttpSecurity http) throws Exception {
    http.formLogin() //表单认证
            .loginPage("xxx.html") //自定义登录页面
            .loginProcessingUrl("/xxx") //表单提交的路径
            .usernameParameter("username")
            .usernameParameter("password") //自定义input的name值
            .successForwardUrl("/") //登录成功之后跳转的路径
            .and().rememberMe()//开启记住我功能
            .tokenValiditySeconds(1209600) //token失效时间 默认是两周
            .rememberMeParameter("remember-me") //自定义表单input值
            .tokenRepository(getPersistentTokenRepository());
            .and()
            .authorizeHttpRequests()
            .antMatchers("xxx.html")
            .permitAll()//放行登录界面
            .anyRequest()
            .authenticated();//所有的请求都需要认证

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

    // 加载同源域名下iframe页面
    http.headers().frameOptions().sameOrigin();
}
  1. 数据库存储展示 image.png

  2. Cookie窃取伪造演示

    • 使用网页登录系统,记录remember-me的值
    • 使用postman伪造cookie
  3. 安全验证

@GetMapping("/{id}")
@ResponseBody
public User getById(@PathVariable Integer id) {
    Authentication authentication = SecurityContextHolder.getContext().getAuthentication();// 获取认证的信息
    //如果返回true, 代表这个登录认证的信息来源于自动登录
    if (RememberMeAuthenticationToken.class.isAssignableFrom(authentication.getClass())) {
        throw new RememberMeAuthenticationException("认证来源于RememberMe");
    }
    User user = userService.getById(id);
    return user;
}

自定义登录成功处理和失败处理

在某些场景下,用户登录成功或失败的情况下用户需要执行一些后续操作,比如登录日志的搜集,或者在目前前后端分离的情况下用户登录和失败后需要给前台页面返回对应的错误信息,有前台主导登录成功或者失败的页面跳转,这个时候需要用的AuthenticationSuccessHandler与AnthenticationFailureHandler.

自定义成功处理: 实现AuthenticationSuccessHandler接口,重写onAnthenticationSuccess()方法。

自定义失败处理: 实现AuthenticationFailureHandler接口,并重写onAuthenticationFailure方法;

  1. 代码实现登录成功或失败的自定义处理

    • SecurityConfiguration类
    
    @Autowired
    MyAuthenticationService myAuthenticationService;
    
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.formLogin() //表单认证
                .loginPage("xxx.html") //自定义登录页面
                .loginProcessingUrl("/xxx") //表单提交的路径
                .usernameParameter("username")
                .usernameParameter("password") //自定义input的name值
                .successForwardUrl("/") //登录成功之后跳转的路径
                .successHandler(myAuthenticationService)
                .failureHandler(myAuthenticationService)
                .and().rememberMe()//开启记住我功能
                .tokenValiditySeconds(1209600) //token失效时间 默认是两周
                 .rememberMeParameter("remember-me") //自定义表单input值
                .tokenRepository(getPersistentTokenRepository())
                .and()
                .authorizeHttpRequests()
                .antMatchers("xxx.html")
                .permitAll()//放行登录界面
                .anyRequest()
                .authenticated();//所有的请求都需要认证
    
        http.csrf().disable(); // 关闭csrf保护
    
        // 加载同源域名下iframe页面
        http.headers().frameOptions().sameOrigin();
    }
    
    • 自定义类实现 AuthenticationSuccessHandler、AuthenticationFailureHandler
    import org.springframework.security.core.Authentication;
    import org.springframework.security.core.AuthenticationException;
    import org.springframework.security.web.DefaultRedirectStrategy;
    import org.springframework.security.web.RedirectStrategy;
    import org.springframework.security.web.authentication.AuthenticationFailureHandler;
    import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
    import org.springframework.stereotype.Service;
    
    import javax.servlet.FilterChain;
    import javax.servlet.ServletException;
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    import java.io.IOException;
    
    /**
     * 自定义登录成功或失败处理器
     */
    @Service
    public class MyAuthenticationService implements AuthenticationSuccessHandler, AuthenticationFailureHandler {
    
        RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();
    
    
        @Override
        public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authentication) throws IOException, ServletException {
            System.out.println("登录成功后继续处理");
            // 重定向到index页面
            redirectStrategy.sendRedirect(request, response, "/");
        }
    
        @Override
        public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
    
        }
    
        @Override
        public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
            System.out.println("登录失败后继续处理");
    
            //重定向到index页面
            redirectStrategy.sendRedirect(request, response, "/toLoginPage");
    
        }
    }
    
  2. 异步登录实现

    • 前端页面改造

    image.png

    • 后端代码
    import com.fasterxml.jackson.databind.ObjectMapper;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.http.HttpStatus;
    import org.springframework.security.core.Authentication;
    import org.springframework.security.core.AuthenticationException;
    import org.springframework.security.web.DefaultRedirectStrategy;
    import org.springframework.security.web.RedirectStrategy;
    import org.springframework.security.web.authentication.AuthenticationFailureHandler;
    import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
    import org.springframework.stereotype.Service;
    
    import javax.servlet.FilterChain;
    import javax.servlet.ServletException;
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    import java.io.IOException;
    import java.util.HashMap;
    import java.util.Map;
    
    /**
     * 自定义登录成功或失败处理器
     */
    @Service
    public class MyAuthenticationService implements AuthenticationSuccessHandler, AuthenticationFailureHandler {
    
        RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();
    
        @Autowired
        ObjectMapper objectMapper;
    
    
        @Override
        public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authentication) throws IOException, ServletException {
            System.out.println("登录成功后继续处理");
            // 重定向到index页面
    //        redirectStrategy.sendRedirect(request, response, "/");
    
            Map result = new HashMap();
            result.put("code", HttpStatus.OK.value()); //200
            result.put("message", "登录成功");
            response.setContentType("application/json;charset=UTF-8");
            response.getWriter().write(objectMapper.writeValueAsString(result));
    
    
    
        }
    
        @Override
        public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
    
        }
    
        @Override
        public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
            System.out.println("登录失败后继续处理");
    
            //重定向到index页面
    //        redirectStrategy.sendRedirect(request, response, "/toLoginPage");
    
            Map result = new HashMap();
            result.put("code", HttpStatus.UNAUTHORIZED.value()); //401
            result.put("message", "失败 ");
            response.setContentType("application/json;charset=UTF-8");
            response.getWriter().write(objectMapper.writeValueAsString(result));
    
    
        }
    }
    ```
    ```
    

退出登录

org.springframework.security.web.authentication.logout.LogooutFilter

匹配URL为/logout的请求,实现用户退出,清除认证信息

只需要发送请求,请求路径为/logout即可,当然这个路径也可以在自行在配置类中自行指定,同时退出操作也有对应的自定义处理LogoutSuccessHandler,退出登录成功后,退出的同时如果有remember-me的数据,同时一并删除。

  • 前端页面

    image.png

  • 后台代码

    • 自定义退出登录处理器
    import com.fasterxml.jackson.databind.ObjectMapper;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.http.HttpStatus;
    import org.springframework.security.core.Authentication;
    import org.springframework.security.core.AuthenticationException;
    import org.springframework.security.web.DefaultRedirectStrategy;
    import org.springframework.security.web.RedirectStrategy;
    import org.springframework.security.web.authentication.AuthenticationFailureHandler;
    import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
    import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
    import org.springframework.stereotype.Service;
    
    import javax.servlet.FilterChain;
    import javax.servlet.ServletException;
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    import java.io.IOException;
    import java.util.HashMap;
    import java.util.Map;
    
    /**
     * 自定义登录成功或失败处理器,退出登录处理器
     */
    @Service
    public class MyAuthenticationService implements AuthenticationSuccessHandler, AuthenticationFailureHandler, LogoutSuccessHandler {
    
        RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();
    
        @Autowired
        ObjectMapper objectMapper;
    
    
        @Override
        public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
            System.out.println("退出登陆成功");
            redirectStrategy.sendRedirect(request, response, "/toLoginPage");
        }
    }
    
    
    • SecurityConfig
    /**
     * http请求
     * @param http
     * @throws Exception
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.formLogin() //表单认证
                .loginPage("xxx.html") //自定义登录页面
                .loginProcessingUrl("/xxx") //表单提交的路径
                .usernameParameter("username")
                .usernameParameter("password") //自定义input的name值
                .successForwardUrl("/") //登录成功之后跳转的路径
                .successHandler(myAuthenticationService)
                .failureHandler(myAuthenticationService)
                .and().logout().logoutUrl("/logout").logoutSuccessHandler(myAuthenticationService)
                .and().rememberMe()//开启记住我功能
                .tokenValiditySeconds(1209600) //token失效时间 默认是两周
                 .rememberMeParameter("remember-me") //自定义表单input值
                .tokenRepository(getPersistentTokenRepository())
                .and()
                .authorizeHttpRequests()
                .antMatchers("xxx.html")
                .permitAll()//放行登录界面
                .anyRequest()
                .authenticated();//所有的请求都需要认证
    
        http.csrf().disable(); // 关闭csrf保护
    
        // 加载同源域名下iframe页面
        http.headers().frameOptions().sameOrigin();
    }
    
    

图形验证码验证

图形验证码一般防止恶意登录,将一串随机产生的数字或符号生成一幅图片,图片里加一些干扰,也有目前需要收到滑动的图形验证,这种有专门去做的第三方平台,比如极验(www.geetes.com/)

Spring Security添加验证码大致可分为三个步骤:

  1. 根据随机数生成验证码图片
  2. 将验证码图片显示到登录页面
  3. 认证流程中加入验证码校验

Spring Security的认证校验是由UsernamePasswordAuthenticationFilter过滤器完成的,所以我们的验证码校验逻辑应该在这个过滤器之前。验证吗通过后才能到后续的操作,流程如下

image.png

session管理

Spring Security可以与Spring Session库配合使用,只需要做一些简单的配置就可以实现一些功能,如:会话过期、一个账号只能同时在线一个、集群session等。

会话超时

  1. 配置session会话超时时间,默认是30分钟,但是Spring Boot的会话超时时间至少为60秒
    #session设置
    #配置session超时时间
   server.servlet.session.timeout=60

当session超时后,默认跳转到登录界面

  1. 自定义设置session超时后地址 设置session管理和失效后跳转地址
    http.sessionManagement()//设置session管理
                            .invalidSessionUrl("/toLoginPage")

2.5.2并发控制

并发控制即同一个账号同时在线个数如果设置为1,表示该账号在同一时间内只能有一个有效的登录,如果一个账号又在其他登录,那么就将上次登录的会话过期,即后面的登录就会踢掉前面的登录

  1. 修改超时时间
    #session设置
    #配置session超时时间
   server.servlet.session.timeout=60
  1. 设置最大会话数量
http.sessionManagement() //设置session管理
    .invalidSessionUrl("toLoginPage") //session失效之后跳转的路径
    .maximumSessions(1) //session最大会话数量1,同一时间只能有一个用户可以登录 互踢
    .expiredUrl("toLoginPage") //session过期之后跳转的路径
;
  1. 阻止用户第二次登录

sessionManagement可以配置maxSessionPreventsLogin:boolean值,当达到maximumSession设置的最大值后阻止登录

http.sessionManagement() //设置session管理
    .invalidSessionUrl("toLoginPage") //session失效之后跳转的路径
    .maximumSessions(1) //session最大会话数量1,同一时间只能有一个用户可以登录 互踢
    .maxSessionsPreventsLogin(true) // 如果达到最大会话数量,就阻止登录(开启后,下一行的代码不起作用)
    .expiredUrl"toLoginPage") //session过期之后跳转的路径
;

集群session

实际场景中一个服务至少会有两台服务器在提供服务,在服务器前面会有一个nginx做负载均衡,用户访问nginx,nginx再决定去访问哪一台服务器。当一台服务器宕机了之后,另一台服务器可以继续提供服务,保证服务不中断。如果我们将session保存再Web(比如tomcat)中,如果一个用户第一次访问被分配到服务器1上面需要登录,当某些访问突然被分配到服务器2上,因为服务器1上登录的session信息,服务器二还会再次让用户登录,用户已经登录了还让登录就感觉非常不正常了。

image.png

解决这个问题的思路是用户登录的会话信息不能再保存到Web服务器中,而是保存到一个单独的库(redis、mongodb、jdbc等)中,所有服务器都访问同一个库,都从同一个库来获取用户的session信息,如用户在服务器1上登录,将会话信息保存到库中,用户的下次球球被分配到服务器2,服务器2从库中,用户的下次请求被分配到服务器2上,服务器2从库中检查session是否已经存在,如果存在就不用登录了,可以直接访问服务了。

image.png

  1. 引用依赖
    <dependecy>
        <groupId>org.springframework.session</groupId>
        <artifactId>spring-session-data-redis</artifactId>
   </dependecy>
  1. 设置session存储类型
        #使用redis共享session
        spring.session.store-type=redis
        
    
  2. 测试
    • 使用其中一个服务去登录http://localhost:8080/login
    • 使用另一个服务去访问任意接口,则不需要再重新登录就可以直接访问

csrf防护机制

什么是csrf

CSRF(Cross-site request forgery),中文名称:跨站请求伪造

可以这样理解:攻击者盗用了你的身份,以你的名义发送恶意请求。CSRF能够做的事情包括:以你的名义发送邮件,发送消息,盗取你的账号,甚至于购买商品,虚拟货币转账......造成个人隐私泄露以及财产安全

CSRF这种攻击方式在2000年已经被国外的安全人员提出,但在国内直到06年才开始被关注,08年国内外的多个大型社区和交互网站分布爆出CSRF漏洞,如:NYTime.com(纽约时报)、Metafilter(一个大型的BLOG网站)、YouTube和百度HI......而现在,互联网上的许多站点仍对此毫无防备,以至于安全业界成为CSRF为“沉睡的巨人”。

CSRF的原理

image.png

从上图可以看出,要完成一次CSRF攻击,受害者必须依次完成三个步骤:

  1. 登录受信任的网站A
  2. 在不退出A的情况下,访问危险网站B。
  3. 触发网站B中的一些元素

CSRF的防御策略

在业界目前防御CSRF攻击主要有三种策略:验证HTTP Referer字段:在请求地址中添加token并验证:在HTTP头中自定义数学并验证。

  1. 验证HTTP Referer

    根据HTTP协议,在HTTp头中有一个字段叫Referer,它记录了该HTTP请求的来源地址。在通常情况下,访问一个安全受限页面的请求来自于同一个网站,在后台请求验证器Referer值,如果是以自身安全网站开头的域名,则说明该请求是合法的。如果Refer是其他网站的话,则有可能是黑客的CSRF攻击,拒绝该请求

  2. 在请求地址中添加token并验证

    CSRF攻击之所以能成功,是因为黑客可以完全伪造用户的请求。该请求所有的用户验证信息都是存在于cookie中,因此黑客可以在不知道这些验证信息的情况下直接利用用户自己的cookie来通过安全验证。要抵御CSRF,关键在于在请求中放入黑客所不能伪造的信息,并且该信息不存在于cookie之中。可以在HTTP请求中以参数的形式加入一个随机产生的token,并在服务器端建立一个拦截器来验证这个token,如果请求中没有token或者token内容不正确,则认为可能是CSRF攻击而拒绝请求。

3.在HTTP头中自定义数学并验证

这种方法也是使用token并验证,和上一中方法不同的是,这里并不是token以参数的形式置于HTTP请求制作,而是把它放到HTTP头中的自定义的数学里。

security中的CSRF防御机制

org.springframework.security.web.csrf.CsrfFilter

csrf又称跨站请求伪造,SpringSecurit会对所有的post请求验证是否包含系统生成的csrf的token信息,如果不包含,则报错。起到防止csrf攻击的效果(1.生成token 2验证token)

  1. 开启csrf防护
   //开启csrf防护,可以设置哪些不需要防护
   http.csrf().ignoreAntMatchers("/user/save)
  1. 页面需要添加token值
    <input type = "hidden" th:name="${_csrf.parameterName}" th:value = "${_csrf:token}">

跨域与CORS

跨域

跨域实质上是浏览器的一种保护处理,如果产生了跨域,服务器在返回结果时就会被浏览器拦截(注意,此时请求是可以正常发起的,只是浏览器对其进行了拦截),导致想要的内容不可用,产生跨域的几种情况有以下:

image.png

解决跨域

  1. JSONP

浏览器允许一些自带src属性的标签跨域,也就是某些标签的src属性上写url地址是不会产生跨域问题。

  1. CORS解决跨域

CORS是一个W3C标准,全称是“跨域资源共享”(Cross-origin resource sharing)。CORS需要浏览器和服务器。目前,所有浏览器都支持该功能,IE浏览器不能低于IE10。浏览器在发起真正的请求之前,会发起一个OPTIONS类型的预检查请求,用于请求服务器是否允许跨域,在得到许可的情况下才会发起请求。

基于Spring Security的CORS支持

  1. 声明跨域配置源
/**
 * 跨域配置信息源
 */
public CorsConfigurationSource corsConfigurationSource() {

    CorsConfiguration corsConfiguration = new CorsConfiguration();
    //允许跨域的站点
    corsConfiguration.addAllowedOrigin("*");
    //允许跨域的http方法
    corsConfiguration.addAllowedMethod("*");

    //允许跨域的请求头
    corsConfiguration.addAllowedHeader("");


    //允许带凭证
    corsConfiguration.setAllowCredentials(true);

    UrlBasedCorsConfigurationSource urlBasedCorsConfigurationSource = new UrlBasedCorsConfigurationSource();
    urlBasedCorsConfigurationSource.registerCorsConfiguration("/**",  corsConfiguration);

    return urlBasedCorsConfigurationSource;
}
  1. 开跨域支持
//开启跨域支持
http.cors().configurationSource(corsConfigurationSource());

Spring Security授权

授权简介

在第二部分中我们讲解的都是用户认证,不管是用户密码,还是图形验证码等,最终的目的都是一个:让系统知道你到底是谁在访问你的系统,解决的问题是你是谁?这部分主要讲解你能在系统中做什么事情,针对这个问题叫做:授权,有的也叫做:鉴权,还有叫权限控制。最终的目的就是你能在系统中能做什么?

Spring Security对授权的定义

image.png

安全权限控制问题其实就是控制能否访问url

Spring Security授权原理

image.png

在我们应用系统里面,如果相关控制用户权限,需要有2部分数据

  1. 系统配置信息数据:写着系统里面有哪些URL,每一个Url拥有哪些权限才允许被访问。

  2. 另一份数据就是用户权限信息:请求用户拥有权限

    系统用户发送一个请求:系统配置信息和用户权限信息作对比,如果对比成功则允许访问。

当一个系统授权规则比较简单,基本不变的时候,系统的权限配置信息可以直接写在我们的代码里面。比如前台的门户网站等权限比较单一,可以使用简单的授权配置即可完成。但如果权限复杂,例如办公OA,电商后台管理系统等就不能卸载代码里面了,需要RABC权限模型设计。

Spring Securiy授权

内置权限表达式

Spring Security使用Spring EL来支持,主要用于Web访问和方法安全上,可以通过表达式来判断是否具有访问权限。下面是Spring Security常用的内置表达式, ExpressionUrlAuthorizationConfigure定义了所有的表达式

image.png

url安全表达式

基于Web访问使用表达式保护url请求路径:

  1. 设置url访问权限

在Web安全表达式中引用自定义Bean授权

  1. 自定义Bean

Method安全表达式

针对方法级别的访问控制比较复杂,Spring Security提供了四种注解分别是@PreAuthorize, @PostAuthorize, @PreFilter, @PostFilter

  1. 开启方法级别的注解配置

    在security配置类中添加注解

基于数据库基于RBAC数据库模型控制权限

我们开发一个系统,必然面临权限控制的问题,不同的用户具有不同的访问、操作、数据权限。形成理论的权限控制模型有:自主访问控制(DAC:Discretionary Access Control)、强制访问控制(MAC:Mandatory Access Control)、基于属性的权限验证(ABAC:Attribute-Based Access Control)等。最长被开发者使用也是相对易用、通用的就是RBAC权限模型(Role-Based Access Control)

RBAC权限模型简介

RBAC权限模型(Role-Based Access Control)即:基于角色的权限控制。模型中有几个关键的术语:

  • 用户:系统接口及访问的操作者
  • 权限:能够访问某接口或者作某操作的授权资格
  • 角色:具有某一类相同操作权限的总称

RBAC权限模型核心授权如下:

  • 某用户是什么角色?
  • 某角色有什么权限?
  • 通过角色对应的权限推导出用户的权限

RBAC的演化进程

  1. 用户与权限直接关联

    image.png

想到权限控制,人们最先想到的一定是用户与权限直接关联的模式,简单地说就是:某些用户具有某些权限。如图:

  • 张三具有所有权限它可莪能是一个超级管理员
  • 李四王五具有添加商品和审核商品的权限有可能是一个普通业务员。

这种模型能够星期的表达用户与权限之间的关系,足够简单,但同时也存在问题。

  • 现在用户是张三、李四、王五,以后随着人员增加,每一个用户都需要重新授权
  • 操作人员他的权限发生变更后,需要对每一个用户重新授予新的权限
  1. 用户与角色关联

image.png

这样只需维护角色和权限之间的关系就可以了,如果业务员的权限发生变更,只需要变动业务员角色和权限之间的关系进行维护就可以了,用户和权限分离开来了,如下图

image.png

基于RBAC设计权限表结构

  • 一个用户有一个或多个角色
  • 一个角色包含多个用户
  • 一个角色有多种权限
  • 一个权限属于多个角色

image.png

基于Spring Security实现RBAC权限管理

  1. 动态查询数据库中用户对应的权限