Spring Security的初级认识

权限管理的核心功能:

  • 认证(你是谁)
  • 授权(你能干什么)
  • 攻击防护(防止伪造身份)

基本原理

Spring Security 过滤器链,即一组 Filter 。每一个 Filter 负责处理一种认证方式 。

例如:

Username Password Authentication Filter 负责检查请求参数中是否有用户名和密码,如果有则进行认证,如果没有则“放过”,将请求交给下一个。如:Basic Authentication Filter 。

Basic Authentication Filter 负责检查请求的请求头中是否有 Basic 开头的 Authentication 信息,如果有则会取出并做 Base64 解码后取出其中的用户名密码作认证,如果没有则”放过 。

FilterSecurityInterceptor 是整个过滤器链的最后一环。它会根据请求无法满足的条件,抛出不同的异常。抛给了它前一个叫 Exception Translation Filter 的过滤器。

Exception Translation Filter 获取所抛出的异常后,会去引导用户去做相应处理。

pom.xml 全家桶

<dependency>
  <groupId>org.springframework</groupId>
  <artifactId>spring-framework-bom</artifactId>
  <version>${spring.version}</version>
  <type>pom</type>
  <scope>import</scope>
</dependency>

pom.xml 自定义最小依赖

<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-core</artifactId>
    <version>${spring.version}</version>
</dependency>
<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-config</artifactId>
    <version>${spring.version}</version>
</dependency>
<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-web</artifactId>
    <version>${spring.version}</version>
</dependency>

1. 登录

1.1 核心配置(Part 1)

启用 Spring Security 的核心配置有四步:

  1. web.xml 中启用 springSecurityFilterChain,进行请求拦截
  2. spring-security.xml 中声明 spring security 相关配置
  3. 必须为 spirng-security 准备好一个密码加密器
  4. 提供当前登录用户密码和权限的“标准答案”

以下只使用/提供 Java 代码配置

/**
 * 该类的存在相当于在 web.xml 中配置了 springSecurityFilterChain 去拦截所有请求
 * 其中不再需要其他内容
 */
public class SecurityWebApplicationInitializer extends AbstractSecurityWebApplicationInitializer {
}

Spring Security 的 Java 代码配置类基本格式如下:

@Configuration
@EnableWebSecurity
public class SpringSecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
    ... //  对参数 http 调用其各种方式即为对 Spring Security 进行各种配置。
    }
}

注意,记得将该配置文件加入 WebAppInitializer 中。

Spring Security 有两种登录认证方式(显示内容,要求用户填写用户名密码):表单页 和 http-basic 。配置它们的方式为:

@Override
protected void configure(HttpSecurity http) throws Exception {
  http.httpBasic(); // 或
  http.formLogin(); // 两者二选一。
}
  • .httpBasic() 方式会在浏览器中出现一个弹框
  • .formLogin() 方式会跳转至 spring-security 内置的登录页
@Override
protected void configure(HttpSecurity http) throws Exception {

  // 登录方式配置
  http.formLogin()

  // url拦截鉴权
  http.authorizeRequests()    // 开始“授权配置”
        .anyRequest()            // 任何请求
        .authenticated();        // 都需要授权

/*
  // 另一种写法
  http.formLogin()
    .and()                             // 逻辑上相当于分隔符
        .authorizeRequests()
        .anyRequest()
        .authenticated();
*/
}

1.2 核心配置(Part 2)

在 Spring-security 中,用户信息的获取逻辑被封装在了 UserDetailsService 接口中。我们必须要实现这个接口,并在其中提供用户信息的“标准答案”。

该接口要求返回的 UserDetails 也是个接口,我们使用的是它的实现类 User 。User 的构造方法要求三个参数:用户名、密码、用户的权限的集合。

public class UserDetailsServiceImpl implements UserDetailsService {

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        return new User("tom", "123", AuthorityUtils.commaSeparatedStringToAuthorityList("ADMIN")); //  这里 admin 的大小写有区别
    }

  // 截止目前为止,配置仍没有结束。项目仍无法运行
}

密码的匹配工作是由 SpringSecurity 来做的,你只需要告诉它你获得的(例如从数据库中)的密码是什么即可。

UserDetails 中封装了用户登录过程中所需的全部信息:

  • isAccountNonExpired(),账户是否过期
  • isAccountNonLocked(),杭虎是否被锁(冻结)
  • isCredentialsNonExpired(),密码是否过期
  • isEnabled(),是否可用(假删除)
return new User("tom", "123456",
                true, true, true, true,
                AuthorityUtils.commaSeparatedStringToAuthorityList("admin"));

任何一个现实中的项目中,都不会将密码以明文的方式存储于数据库中。SpringSecurity 要求你提供/指定一个密码加密器。

SpringSecurity 使用 org.springframework.security.crypto.password.PasswordEncoder 对你提供的密码进行加密。该接口中有两个方法:加密方法,是否匹配方法。

加密方法(encode)方法用户注册功能在获得用户的密码后对其加密。

而另一个方法 matches 方法是由 Spring Security 调用的,它用该方法来比较登录密码和密码“标准答案”。

原本 Spring Security 中提供的不少内置的已实现的加密器,不过,Spring Security5 为了解耦,大量原有的加密器被标记为废弃,转由用户“额外”提供。仅剩的加密器还有:

  • BCryptPasswordEncoder(官方推荐使用)
  • Pbkdf2PasswordEncoder
  • SCryptPasswordEncoder
@Configuration
@EnableWebSecurity
public class SpringSecurityConfig extends WebSecurityConfigurerAdapter {

    @Bean("passwordEncoder")
    public PasswordEncoder passwordEncoder() {
    return NoOpPasswordEncoder.getInstance();
//        return new BCryptPasswordEncoder();
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
    ...
    }
}

另外,Spring Security 提供了一个叫 NoOpPasswordEncoder 是一个”无意义“加密器,它对原始密码没有做任何处理(现在也被标记为废弃)。

1.3 指定登录后的页面

登录成功后的跳转页面/路径有两种:

  • 默认情况下,我们在登录成功后会返回到原本受限制的页面/请求。
  • 但如果用户是直接请求登录页面,登录成功后默认情况下是跳转到当前应用的根路径,即欢迎页面。
// 登录页面配置
http.formLogin()
      .defaultSuccessUrl("/success.jsp");
//  .defaultSuccessUrl("/success.jsp", true);

通过 .defaultSuccessUrl() 可以指定上述第二种情况下的成功跳转页面。如果多加一个参数true,那么也就涵盖了第一种情况下的成功跳转页面。

类似的,通过 .failureForwardUrl() 可以指定登录失败时跳转的错误页面。

1.4 自定义登录页面和登录请求

@Configuration
@EnableWebSecurity
public class SpringSecurityConfig extends WebSecurityConfigurerAdapter {

  ...

    @Override
    protected void configure(HttpSecurity http) throws Exception {
          http.formLogin()
              .loginPage("/login.html"); // 指定登录页面。

          http.authorizeRequests()
        .antMatchers("/login.html").permitAll()    // 这句配置很重要,新手容易忘记。放开 login.html 的访问权
              .anyRequest()
              .authenticated();
    }
}

在 SpringSecurity 中 form 表单方式的登录处理是由 UsernamePasswordAuthenticationFilter 处理的,在这个 filter 中

  • 默认的登录请求 url 是 /login
  • 默认的两个请求参数分别是 username 和 password
  • 默认的请求方式是 post

要么你自定义的登录页面必须满足以上默认的条件,要么进行配置,手动指定。

@Override
protected void configure(HttpSecurity http) throws Exception {
  http.formLogin()
      .loginPage("/sign-in.html")
          .loginProcessingUrl("/login");     // 注意,此处路径中没有项目名作前缀
      // .usernameParameter("username")
      // .passwordParameter("password")

    http.authorizeRequests()
          .antMatchers("/sign-in.html").permitAll()
          .anyRequest()
          .authenticated()

  http.csrf()
      .disable();    //  关闭 csrf 功能
}

默认,Spring Security 开起了 CSRF Token 功能(跨站请求伪造攻击防护),因此,此时需要通过配置先关闭掉。

但是有时候(其实就是 Rest-ful 风格的API)中,你需要的并不是页面跳转,而是服务端返回 JSON 格式的数据,其中包含登录成功或失败信息。


1.5 Remember Me

Spring-Security 功能的基本步骤和原理:

  1. 用户请求会被 SpringSecurityFilterChain 中的 UsernamePasswordAuthenticationFilter 拦截处理
  2. 一旦用户认证成功,该 Filter 会去调用 RememberMeService ,而 RememberMeService 会生成一个 Token
  3. RememberMeService 一方面会回给浏览器一个包含了该 token 的 cookie ,另一个方面会将该 token 存入数据库表中
  4. 在数据库中,登录用户的用户名(username)和 Token 是一一对应的。
  5. 在以后的请求中,浏览器会携带包含了用户 token 的 cookie,而 RememberMeAuthenticationFilter,它负责从请求的提取该 Cookie 中的 token 并与数据库中的 token进行比较。
  6. 比对成功后,还需要进一步去调用 UserDetailsService 会执行登录操作,从而实现自动登录功能。

RememberMeService 向数据库写入用户的 Token 信息需要借助于 JdbcTokenRepository 接口及其实现类 JdbcTokenRepositoryImpl。 JdbcTokenRepositoryImpl 中有现成的数据库语句。

注意,页面上的 checkbox 的 name 必须为 remember-me :

<input type="checkbox" id="remember-me" name="remember-me" />自动登录

在 spring-security.xml 中配置 Remember Me 功能核心配置有两步:

  1. 配置用于操作数据库存取用户 Token 的 Repository(即 Dao)
  2. 配置后续进行验证的 UserDetailService 。
@Override
protected void configure(HttpSecurity http) throws Exception {

        // 登录页面配置
    ...

        // 自动登录功能配置
        http.rememberMe()
            .tokenRepository(persistentTokenRepository()) // 步骤 1
            .tokenValiditySeconds(60)
            .userDetailsService(userDetailsService); // 步骤 2

         // 鉴权配置
     ...

        // 关闭 csrf 功能
        ...
    }

由于涉及到数据库的操作,所以 spring-security.xml 中配置 remember-me 功能时,依赖于 spring-dao.xml 中的 database :

@Autowired
private DataSource dataSource;

另外,Spring-Security 是通过 JdbcTokenRepository 及其实现类 JdbcTokenRepositoryImpl 对数据库进行读写操作:

@Bean
public PersistentTokenRepository persistentTokenRepository() {
    JdbcTokenRepositoryImpl repository = new JdbcTokenRepositoryImpl();
        repository.setDataSource(dataSource);
        // repository.setCreateTableOnStartup(true);  // 让 Spring-Security 创建表

    return repository;
}

1.6 自定义登录成功/失败处理

有时候(特别是 Rest-ful 风格的 web 项目)登录成功和失败之后的“结果”并不是页面跳转,在前端发出 ajax 请求后,你的返回结果可能就只是一个证明验证成功或失败的 JSON 字符串(特别是登录失败的情况下)。

Spring-Security 提供了一种机制:你可以向 Spring-Security 配置一个 Handler,在验证成功(或失败)后,Spring-Security 来调用这个 Handler 中的指定方法。至于接下来是页面的跳转,还是返回 JSON 格式数据,完全取决于你所实现的这个 Handler 中的方法的执行结果。

以下以失败处理为例,成功处理与它类似。

1.6.1 定义 Handler

处理登录成功之后的后续 Handler 必须实现 AuthenticationSuccessHandler;处理登录失败之后的后续 Handler 必须实现 AuthenticationFailureHandler

经过注册后,

  • Spring-Security 在验证用户登录成功之后,会去调用/执行 AuthenticationSuccessHandler 中的 onAuthenticationSuccess() 方法
  • Spring-Security 在验证用户登录失败之后,会去调用/执行 AuthenticationFailureHandler 中的 onAuthenticationFailure() 方法

如果,你期望登录失败之后,是通过 转发 进行跳转显示失败页面,那么:

@Override
public void onAuthenticationFailure(HttpServletRequest request,
        HttpServletResponse response, AuthenticationException exception)
        throws IOException, ServletException {
        System.out.println("登录失败");
        request.getRequestDispatcher("/failure.jsp").forward(request, response);
}

如果,你期望登录失败之后,是通过 重定向 进行跳转显示失败页面,那么:

@Override
public void onAuthenticationFailure(HttpServletRequest request,
        HttpServletResponse response, AuthenticationException exception)
        throws IOException, ServletException {
        System.out.println("登录失败");
    response.sendRedirect("/xxx/failure.jsp");
}

如果,你期望登录失败之后,向客户端浏览器发送 JSON 格式数据,那么:

@Override
public void onAuthenticationFailure(HttpServletRequest request,
        HttpServletResponse response, AuthenticationException exception)
        throws IOException, ServletException {
  System.out.println("登录失败");
  response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
  response.setContentType("application/json;charset=UTF-8");
  response.getWriter().write("json 格式字符串");
}

考虑到 Spring MVC 默认是使用 jackson 库作 JSON 字符串的转换,我们可以在 spring-security.xml 配置一个 jackson 的 ObjectMapper 并在此处用于对象转JSON。

@Autowired
private ObjectMapper objectMapper;

@Override
public void onAuthenticationFailure(HttpServletRequest request,
        HttpServletResponse response, AuthenticationException exception)
        throws IOException, ServletException {
        System.out.println("登录失败");
        response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
        response.setContentType("application/json;charset=UTF-8");
        response.getWriter().write(objectMapper.writeValueAsString(对象));
}

1.6.2 注册 Handler

@Configuration
@EnableWebSecurity
@ComponentScan("com.sxnd.authentication")
public class SpringSecurityConfig extends WebSecurityConfigurerAdapter {

    ...

/*
    @Autowired
    @Qualifier("myAuthenticationSuccessHandler")
    private AuthenticationSuccessHandler successHandler;
*/

    @Autowired
    @Qualifier("myAuthenticationFailureHandler")  // Spring-Security 会有多个 FailureHandler 因此需要明确指明 id
    private AuthenticationFailureHandler failureHandler;

    @Bean("objectMapper")
    public ObjectMapper objectMapper() {
        return new ObjectMapper();
    }

    ...

    @Override
    protected void configure(HttpSecurity http) throws Exception {

        // 登录页面配置
        http.formLogin()
            .loginPage("/sign-in.html")
                .loginProcessingUrl("/login")
//          .successHandler(successHandler)
                .failureHandler(failureHandler);

        ...
    }
}

1.7 退出(logout)

退出的配置所涉及的概念与登录配置类似:

http.logout()
    .logoutUrl("/logout")                    // 触发退出功能的 url
    .logoutSuccessUrl("/index.jsp")      // 退出成功后的跳转页面
    .invalidateHttpSession(true)         // 让当前session失效,默认值就是true
    .deleteCookies("JSESSIONID", ...)    // 退出需要客户端删除的 cookie 名称,多个cookie之间以逗号分隔。
;

同样,logout 也有 Handler 机制,由你来指定退出成功后是进行页面跳转,还是发送 JSON 格式数据。


2. 鉴权

用户看不见,不等于不能访问:页面上不可见是用户体验问题,用户不能访问,是安全问题。

UserDetailsService 的 loadUserByUsername() 所返回的 User 对象的 authorities 参数决定了当前用户所拥有的权限。

权限表达式说明
permitAll()永远返回 true
denyAll()永远返回 false
anonymous()当前用户是匿名用户(anonymous)时返回 true
rememberMe()当前用户是 rememberMe 用户时返回 true
authentication当前用户不是匿名用户时,返回 true
fullyAuthenticated当前用户既不是匿名用户,也不是 rememberMe 用户是,返回 true
hasRole("role")当用户拥有指定身份时,返回 true
hasAnyRole("role1","role2", ...)当用户返回指定身份中的任意一个时,返回 true
hasAuthority("authority1")当用于拥有指定权限时,返回 true
hasAnyAuthority("authority1", "authority2")当用户拥有指定权限中的任意一个时,返回 true
hasIpAddress("xxx.xxx.x.xxx")发送请求的 ip 符合指定时,返回 true
principal允许直接访问主体对象,表示当前用户

注意:hasRole() 会在参数字符串前加 ROLE_,即,hashRole("ADMIN") 真正验证的身份是 ROLE_ADMIN;而 hasAuthority() 则不会加任何前缀。

.antMatchers("/admin_role.jsp").hasRole("ADMIN")    // 配置权限时,此处没有 ROLE_
.antMatchers("/root_role.jsp").hasRole("ROOT")
.antMatchers("/write_author.jsp").hasAuthority("write")
.antMatchers("/read_author.jsp").hasAuthority("read")
return new User("tom", "123456", AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_ADMIN, read")); //  但是权限本身是由 ROLE_ 的

通用的 RBAC(Role-Based Access Control) 数据模型

三张实体表 + 两张关系(中间)表

  • 用户表
  • 角色表
  • 资源表
.anyRequest().access("@rbacService.hasPermission(request, authentication)")
@Component("rbacService")
public class RBACServiceImpl {

    private AntPathMatcher matcher = new AntPathMatcher();

    public boolean hasPermission(HttpServletRequest request, Authentication authentication) {
        Object principal = authentication.getPrincipal();
        boolean permission = false;

        if (principal instanceof UserDetails) {
            String username = ((UserDetails) principal).getUsername();
            Set<String> urls = new HashSet<>(); // 应该来源于数据库
            for (String url : urls) { // url 中有可能有通配符 *,因此不要使用 equals 比较
                if (matcher.match(url, request.getRequestURI())) {
                    permission = true;
                    break;
                }
            }
        }

        return permission;
    }
}
SET FOREIGN_KEY_CHECKS = FALSE;

DROP TABLE IF EXISTS sys_users;
DROP TABLE IF EXISTS sys_roles;
DROP TABLE IF EXISTS sys_permissions;
DROP TABLE IF EXISTS sys_users_roles;
DROP TABLE IF EXISTS sys_roles_permissions;

CREATE TABLE sys_users (
  id bigint auto_increment,
  username varchar(100),
  password varchar(100),
  salt varchar(100),
  locked bool default false,
  PRIMARY KEY (id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

CREATE TABLE sys_roles (
  id bigint auto_increment,
  role varchar(100),
  description varchar(100),
  available bool default false,
  PRIMARY KEY (id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

CREATE TABLE sys_permissions (
  id bigint auto_increment,
  permission varchar(100),
  description varchar(100),
  available bool default false,
  PRIMARY KEY (id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

CREATE TABLE sys_users_roles (
  user_id bigint,
  role_id bigint,
  PRIMARY KEY (user_id, role_id),
  FOREIGN KEY (user_id) REFERENCES sys_users(id),
  FOREIGN KEY (role_id) REFERENCES sys_roles(id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

CREATE TABLE sys_roles_permissions (
  permission_id bigint,
  role_id bigint,
  PRIMARY KEY (role_id, permission_id),
  FOREIGN KEY (role_id) REFERENCES sys_role_permission(id),
  FOREIGN KEY (permission_id) REFERENCES sys_permission(id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

SET FOREIGN_KEY_CHECKS = TRUE;

select * from INFORMATION_SCHEMA.KEY_COLUMN_USAGE where TABLE_SCHEMA = 'scott';
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值