Spring Security

⒈添加Spring Boot Security Starter依赖

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>

  当Spring Boot的自动装配检测到Spring Secutity出现在了类路径中,将会初始化一些基本的安全配置。

  此时启动应用,应用将会弹出一个HTTP Basic认证对话框并提示你进行认证,用户名为user,密码是随机生成的。它会被写入到应用的日志文件中。输入了正确的用户名和密码后,你就有权限访问应用了。

  Spring Security自动装配提供了以下安全特性:

  • 所有的HTTP请求路径都需要认证【这包括你的首页】
  • 不需要特定的角色和权限
  • 没有登录页面
  • 认证过程是通过HTTP Basic认证对话框实现的
  • 系统只有一个用户,用户名为user。

 

⒉配置Spring Security

  基于XML来配置Spring Security不在本文的讨论之内,建议移步其他文章。

  基于Java来配置Spring Security首先你需要继承Spring 提供的适配器

package cn.coreqi.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
}

  此时,Spring的上下文容器中注册了我们自己的Security配置,Spring Security发生了改变,现在不再是提示HTTP Basic认证的对话框,而是会展现一个登录表单。

 

3.配置Spring Security用户存储

  Spring Security为配置用户存储提供了多个可选解决方案,包括:

  • 基于内存的用户存储
  • 基于JDBC的用户存储
  • 以LDAP作为后端的用户存储
  • 自定义用户详情服务

  不管使用哪种用户存储,都可以通过 WebSecurityConfigurerAdapter 基础配置类中定义的configure()方法来进行配置,首先我们需要重写configure()方法。

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

  1.基于内存的用户存储

  Spring Security支持将用户信心存储在内存之中,假设我们的应用只有数量有限的几个用户,而且这些用户几乎不会发生变化,在这种情况下,将这些用户定义在内存中是非常理想的实现方式。

  

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.inMemoryAuthentication()
                .withUser("fanqi")
                    .password("fanqisoft")
                    .authorities("ROLE_ADMIN","ROLE_USER")
                .and()
                .withUser("zhangsan")
                    .password("123456")
                    .authorities("ROLE_USER");
    }

  对于测试和简单的应用来讲,基于内存的用户存储是很有用的,但是这种方式不能很方便的编辑用户。如果需要新增、移除或变更用户,那么你要对代码做出必要的修改,然后重新构建和部署应用。

 

  2.基于JDBC的用户存储

  用户信息通常会在关系型数据库中进行维护,基于JDBC的用户存储方案会更加合理一些。

    @Autowired
    private DataSource dataSource;

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

  在这里的configure()实现中,调用了AuthenticationManagerBuilder 的 jdbcAuthentication()方法。我们还必须要设置一个DataSource,这样它才能知道如何访问数据库。

  重写默认的用户查询功能

  虽然短短的两句话就足以让Spring Security使用JDBC进行用户存储,但它同样也对我们的数据库模式有了一定的要求。

  让我们查看一下Spring Security JDBC用户存储下默认的实现【org.springframework.security.core.userdetails.jdbc.JdbcDaoImpl】,节选部分代码、

    public static final String DEF_USERS_BY_USERNAME_QUERY = "select username,password,enabled from users where username = ?";
    public static final String DEF_AUTHORITIES_BY_USERNAME_QUERY = "select username,authority from authorities where username = ?";
    public static final String DEF_GROUP_AUTHORITIES_BY_USERNAME_QUERY = "select g.id, g.group_name, ga.authority from groups g, group_members gm, group_authorities ga where gm.username = ? and g.id = ga.group_id and g.id = gm.group_id";

  在第一个查询中,我们获取了用户的用户名、密码以及是否启用的信息,用来进行用户认证。

  在第二个查询中,我们查找了所授予用户的权限,用来进行鉴权。

  在第三个查询中,我们查找了用户作为群组的成员所授予的权限。

  如果你能够在数据库中定义和填充满足这些查询的表,那么基本上就不需要再做什么额外的事情了。但是,也有可能你的数据库与上述的不一致,那么你会希望在查询上有更多的控制权。

    @Autowired
    private DataSource dataSource;

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.jdbcAuthentication()
                .dataSource(dataSource)
                .usersByUsernameQuery(
                        "select username,password,enabled from t_user where username = ?"
                )
                .authoritiesByUsernameQuery(
                        "select username,authority from t_authoritie where username = ?"
                )
                .groupAuthoritiesByUsername(
                        "select g.id, g.group_name, ga.authority from groups g, group_members gm, group_authorities ga where gm.username = ? and g.id = ga.group_id and g.id = gm.group_id"
                );
    }

  将默认的SQL查询替换为自定义的设计时,很重要的一点就是要遵循查询的基本协议。所有查询都将用户名作为唯一的参数。认证查询会选取用户名、密码以及启用状态信息。权限查询会选取0行或者多行包含该用户名及其权限信息的数据。群组权限查询会选取0行或多行数据,每行数据中都会包含群组ID、群组名称以及权限。

  指定密码转码器【现在还有人用明文保存密码?】

  通过passwordEncoder()方法指定一个密码转码器(encoder)

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.jdbcAuthentication()
                .dataSource(dataSource)
                .passwordEncoder(PasswordEncoderFactories.createDelegatingPasswordEncoder());
    }

  passwordEncoder()方法可以接收Spring Security中passwordEncoder接口的任意实现。

  以下为Spring Security内置的常用passwordEncoder 接口实现。

  

BCryptPasswordEncoder
使用bcrypt强哈希加密
NoOpPasswordEncoder
不进行任何转码
Pbkdf2PasswordEncoder
使用PBKDF2加密
SCryptPasswordEncoder
使用scrypt哈希加密
StandardPasswordEncoder
使用SHA-256哈希加密

   如果内置的实现无法满足需求时,你甚至可以提供自定义的实现。PasswordEncoder接口非常简单:

public interface PasswordEncoder {

    String encode(CharSequence rawPassword);

    boolean matches(CharSequence rawPassword, String encodedPassword);

    default boolean upgradeEncoding(String encodedPassword) {
        return false;
    }

}

  无论使用哪种密码转码器,重要的是要理解数据库中的密码永远不会被解码。相反,用户在登录时输入的密码使用相同的算法进行编码,然后将其与数据库中编码的密码进行比较。比较是在 PasswordEncoder 的 matches() 方法中进行的。

 

  3.以LDAP作为后端的用户存储

  LDAP(Lightweight Directory Access Protocol,轻量级目录访问协议),为了配置Spring Security使用基于LDAP认证,我们可以使用ldapAuthentication() 方法。这个方法在功能上与 jdbcAuthentication() 类似。

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.ldapAuthentication()
                .userSearchFilter("(uid={0})")
                .groupSearchFilter("member={0}");
    }

  userSearchFilter() 和 groupSearchFilter() 方法用来为基础 LDAP 查询提供过滤条件,它们分别用于搜索用户和组。默认情况下,对于用户和组的基础查询都是空的,也就是表明搜索会在LDAP 层次结构的根目录进行搜索。但是我们可以通过指定查询基础来改变这个默认行为:

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.ldapAuthentication()
                .userSearchBase("ou=people")
                .userSearchFilter("(uid={0})")
                .groupSearchBase("ou=groups")
                .groupSearchFilter("member={0}");
    }

  userSearchBase() 方法为查找用户提供了基础查询。同样,groupSearchBase() 为查找组指定了基础查询。这个示例中我们声明用户应该在名为people的组织单元下搜索而不是从根开始,而组应该在名为groups的组织单元下搜索。

  配置密码对比

  基于 LDAP 身份认证的默认策略是进行绑定操作,直接通过 LDAP 服务器认证用户。另一种可选的方式是进行比对操作。这涉及将输入的密码发送到 LDAP 目录上,并要求服务器将这个密码与用户的密码进行比对。因为比对是在 LDAP 服务器中进行的,所以实际的密码是保密的。

  如果你希望通过密码比对进行身份验证,可以通过声明 passwordCompare() 方法来实现:

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.ldapAuthentication()
                .userSearchBase("ou=people")
                .userSearchFilter("(uid={0})")
                .groupSearchBase("ou=groups")
                .groupSearchFilter("member={0}")
                .passwordCompare();
    }

  默认情况下,在登录表单中提供的密码将与用户 LDAP 条目中的 userPassword 属性值进行比对。如果密码被保存在不同的属性中,可以使用 passwordAttribute() 方法来指定密码属性的名称:

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.ldapAuthentication()
                .userSearchBase("ou=people")
                .userSearchFilter("(uid={0})")
                .groupSearchBase("ou=groups")
                .groupSearchFilter("member={0}")
                .passwordCompare()
                .passwordEncoder(new BCryptPasswordEncoder())
                .passwordAttribute("passcode");
    }

  在本例中,我们指定了要与给定密码进行比对的是“passcode”属性。另外,我们还可以指定密码转码器。在进行服务器端密码比对时,有一点非常好,那就是实际的密码在服务器端是私密的。但是进行尝试的密码还是需要通过网络线路传输到 LDAP 服务器上,这可能会被黑客所截获。为了防止这种情况,我们可以通过调用 passwordEncoder() 方法来指定加密策略。

  在前面的示例中,密码使用 bcrypt 密码哈希函数加密,这需要 LDAP 服务器上的密码也使用 bcrypt 进行了加密。

  引用远程 LDAP 服务器

  到目前为止,我们忽略的一件事就是 LDAP 服务器和实际的数据在哪里?虽然已经将 Spring 配置为根据 LDAP 服务器进行身份验证,但是该服务器在哪里呢?

  默认情况下,Spring Security 的 LDAP 身份验证将假设 LDAP 服务器监听本机的 33389端口。但是,如果 你的LDAP 服务器位于另一台机器上,那么可以使用 contextSource() 方法来配置这个地址:

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.ldapAuthentication()
                .userSearchBase("ou=people")
                .userSearchFilter("(uid={0})")
                .groupSearchBase("ou=groups")
                .groupSearchFilter("member={0}")
                .passwordCompare()
                .passwordEncoder(new BCryptPasswordEncoder())
                .passwordAttribute("passcode")
                .and()
                .contextSource()
                .url("ldap://tacocloud.com:389/dc=tacocloud,dc=com");
    }

  contextSource() 方法会返回一个 ContextSourceBuilder对象,这个对象除了其他功能以外,还提供了 url() 方法来指定 LDAP 服务器的地址。

  配置嵌入式的LDAP服务器

  如果你没有 LDAP 服务器去做身份认证,Spring Security 还为我们提供了嵌入式的 LDAP 服务器。我们不再需要设置远程LDAP服务器的URL,只需通过 root() 方法指定嵌入式服务器的根前缀就可以了。

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.ldapAuthentication()
                .userSearchBase("ou=people")
                .userSearchFilter("(uid={0})")
                .groupSearchBase("ou=groups")
                .groupSearchFilter("member={0}")
                .passwordCompare()
                .passwordEncoder(new BCryptPasswordEncoder())
                .passwordAttribute("passcode")
                .and()
                .contextSource()
                .root("dc=tacocloud,dc=com");
    }  

  当 LDAP 服务器启动时,它会尝试从类路径下寻找 LDIF 文件来加载数据。LDIF(LDAP Data Interchange Format,LDAP 数据交换格式)是以文本文件展示 LDAP 数据的标准方式,每条记录可以有一行或多行,每行包含一个 name:value 配对信息,记录之间通过空行进行分隔。

  如果你不想让 Spring 从整个类路径下寻找 LDIF 文件,那么可以通过调用 ldif() 方法来明确指定加载哪个 LDIF 文件:

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.ldapAuthentication()
                .userSearchBase("ou=people")
                .userSearchFilter("(uid={0})")
                .groupSearchBase("ou=groups")
                .groupSearchFilter("member={0}")
                .passwordCompare()
                .passwordEncoder(new BCryptPasswordEncoder())
                .passwordAttribute("passcode")
                .and()
                .contextSource()
                .root("dc=tacocloud,dc=com")
                .ldif("classpath:users.ldif");
    }

  在这里,我们明确要求 LDAP 服务器从类路径根目录下的 users.ldif 文件中加载数据。如果你比较好奇,如下就是一个包含用户数据的LDIF 文件,我们可以使用它来加载嵌入式 LDAP 服务器:

dn: ou=groups,dc=tacocloud,dc=com
objectclass: top
objectclass: organizationalUnit
ou: groups
dn: ou=people,dc=tacocloud,dc=com
objectclass: top
objectclass: organizationalUnit
ou: people
dn: uid=buzz,ou=people,dc=tacocloud,dc=com
objectclass: top
objectclass: person
objectclass: organizationalPerson
objectclass: inetOrgPerson
cn: Buzz Lightyear
sn: Lightyear
uid: buzz
userPassword: password
dn: cn=tacocloud,ou=groups,dc=tacocloud,dc=com
objectclass: top
objectclass: groupOfNames
cn: tacocloud
member: uid=buzz,ou=people,dc=tacocloud,dc=com

  Spring Security 内置的用户存储非常方便,并且涵盖了最为常用的用户场景。但是,当你的 应用程序需要一些特殊的功能。当开箱即用的用户存储无法满足需求的时候,我们就需要创建和配置自定义的用户详情服务。

  4.自定义用户认证

    1.新建用户实体类并实现UserDetails接口

@Entity
@Data
@NoArgsConstructor(access=AccessLevel.PRIVATE, force=true)
@RequiredArgsConstructor
public class User implements UserDetails {
    private static final long serialVersionUID = 1L;
    
    @Id
    @GeneratedValue(strategy=GenerationType.AUTO)
    private Long id;
    
    private final String username;
    private final String password;
    private final String fullname;
    private final String street;
    private final String city;
    private final String state;
    private final String zip;
    private final String phoneNumber;
    
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return Arrays.asList(new SimpleGrantedAuthority("ROLE_USER"));
    }
    
    @Override
    public boolean isAccountNonExpired() {
        return true;
    }
    
    @Override
    public boolean isAccountNonLocked() {
        return true;
    }
    
    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }
    
    @Override
    public boolean isEnabled() {
        return true;
    }
}

  我们的用户实体类实现了Spring Security的UserDetails接口

  通过实现UserDetails 接口,我们能够提供更多信息给框架,比如用户都被授予了哪些权限以及用户的帐户是否启用。

  getAuthorities() 方法应该返回用户被授予权限的一个集合。各种 is...Expired() 方法要返回一个boolean值,表明用户的帐户是否可用或过期。

 

    2.定义了 User 实体后,现在可以定义repository接口了

public interface UserRepository extends CrudRepository<User,Long> {
    User findByUsername(String username);
}

    3.创建用户详情接口【需要实现UserDetailsService接口】

@Service
public class UserServiceImpl implements UserDetailsService {

    @Autowired
    private UserRepository userRepository;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        User user = userRepository.findByUsername(username);
        if (user != null) {
            return user;
        }
        throw new UsernameNotFoundException("User '" + username + "' not found");
    }
}

    4.将自定义的用户详情服务与Spring Security配置在一起。

    @Qualifier("userServiceImpl")
    @Autowired
    private UserDetailsService userDetailsService;

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

  在这里, 我们只是简单的调用userDetailsService()方法,并将自动装配到Spring Security Config中的UserDetailsService实例传递了进去即可连接到Spring Security的自定义认证服务。

  像基于JDBC的认证一样,我们可以(也应该)配置一个密码转码器,这样在数据库中的密码将是转码过的。我们首先需要声明一个PasswordEncoder类型的Bean,然后通过调用passwordEncoder()方法将它注入到用户详情服务中:

    @Qualifier("userServiceImpl")
    @Autowired
    private UserDetailsService userDetailsService;

    @Bean
    public PasswordEncoder encoder(){
        return PasswordEncoderFactories.createDelegatingPasswordEncoder();
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService)
                .passwordEncoder(encoder());
    }

 

⒊保护Web请求

  当配置完上面的内容后,你会发现你的应用必须获取身份认证才可以正常访问,而我们应用中的一些页面(例如主页、登录页面及注册页面等等)并不需要身份认证便可以访问,为了配置这些安全性规则,需要介绍一下WebSecurityConfigurerAdapter 的另一个 configure() 方法:

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

  这个 configure() 方法接受一个 HttpSecurity 对象,可以使用该对象来配置如何在 web 级别处理安全性。我们可以使用 HttpSecurity 配置的功能包括:

  • 在为某个请求提供服务之前,需要预先满足特定的条件

  • 配置自定义登录页面

  • 支持用户退出应用

  • 配置跨站请求伪造保护

  配置 HttpSecurity常见的需求就是拦截请求以确保用户具有适当的权限。

  1. 保护请求

  我们需要确保只有经过身份认证的用户才能对“/design”和“orders”发起请求,而其他请求则对所有用户均可用。

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .antMatchers("/design", "/orders")
                .hasRole("ROLE_USER")
                .antMatchers("/", "/**").permitAll();
    }

  对 authorizeRequests() 的调用会返回一个对象(ExpressionInterceptUrlRegistry),基于它我们可以指定 URL 路径和这些路径的安全需求。本示例中,我们指定了两条安全规则:

  • 具备ROLE_USER 权限的用户才能访问 “/design” 和 “/orders” 

  • 其他的请求允许所有用户访问

  这些规则的顺序是很重要的。声明在前面的安全规则比后面声明的规则有更高的优先级。如果我们交换这两个安全规则的顺序,那么所有的请求都会有permitAll()的规则,对“ /design ”和 “/orders ”声明的规则就不会生效 了。

  在声明请求路径的安全需求时,hasRole() 和 permitAll() 方法只是众多方法中的两个。下表中列出了所有可用的方法。

方法 能够做什么
access(String)

 

如果给定的 SpEL 表达式计算结果为 true,就允许访问

anonymous() 允许匿名用户访问
authenticated() 允许认证过的用户访问
denyAll() 无条件拒绝所有访问
fullyAuthenticated()

如果用户是完整认证的(不是通过Remember-me功能认证的),

则允许访问

hasAnyAuthority(String...) 如果用户具备给定权限中的某一个,则允许访问
hasAnyRole(String...)

 

如果用户具备给定角色中的某一个,则允许访问

hasAuthority(String)

 

如果用户具备给定权限,则允许访问

hasIpAddress(String)

 

如果请求来自给定 IP ,则允许访问

hasRole(String)

 

如果用户具备给定角色,则允许访问

not()

 

拒绝任何其他访问方法【对其他访问方法的结果求反】

permitAll()

 

无条件允许访问

rememberMe() 如果用户是通过Remember-me功能认证的,则允许访问

 

  上表中的大多数方法为请求处理提供了基本的安全规则,但是它们是自我限制的,也就是只能支持由这些方法所定义的安全规则。除此之外,我们还可以使用 access() 方法,通过为其提供 SpEL 表达式来声明更丰富的安全规则。Spring Security 扩展了 SpEL,包含了多个安全相关的值和函数,如下表所示。

安全表达式 计算结果
authentication

 

用户的认证对象

denyAll 结果始终为false【通常值为 false】
hasAnyRole(list of roles) 如果用户被授予了列表中任意的指定角色,则为 true
hasRole(role) 如果用户被授予了指定的角色,则为 true
hasIpAddress(IP Address)

 

如果请求来自指定 IP 地址,则为 true

isAnonymous() 如果当前用户为匿名用户,则为 true
isAuthenticated()

 

如果当前用户进行了认证,则为 true

isFullyAuthenticated()

如果当前用户进行了完整认证(不是通过Remember-me功能进行

的认证),则为 true

isRememberMe()

 

如果当前用户是通过Remember-me自动认证的,则为 true

permitAll()

 

结果始终为true【通常值为 true】

principal

 

用户的 pricipal 对象

 

   我们可以看到,上表中的大多数安全表达式扩展都对应于上上表中的类似方法。实际上,借助 access() 方法和 hasRole() 、 permitAll 表达式,我们可以按如下方式重写 configure()。

 

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .antMatchers("/design", "/orders")
                .access("hasRole('ROLE_USER')")
                .antMatchers("/","/**").access("permitAll");
    }

  看上去,这似乎没什么大不了的。毕竟,这些表达式只是模拟了我们之前通过方法调用已经完成的事情。但是表达式可以更加灵活。例如,假设(基于某种疯狂的原因)我们只允许具有 ROLE_USER 权限的用户在周二创建新的用户;你可以重写表达式如下:

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .antMatchers("/design", "/orders")
                .access("hasRole('ROLE_USER') && " +
                        "T(java.util.Calendar).getInstance().get("+
                        "T(java.util.Calendar).DAY_OF_WEEK) == " +
                        "T(java.util.Calendar).TUESDAY")
                .antMatchers("/", "/**").access("permitAll");
    }

  我们可以使用SpEL实现各种各样的安全性限制。

  2.创建自定义的登录页

  为了替换Spring Security内置的登录页面,首先需要告诉 Spring Security 自定义登录页面的路径。这可以通过调用传入到 configure() 的 HttpSecurity 对象的 formLogin() 方法来实现:

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .authorizeRequests()
                .antMatchers("/design", "/orders")
                    .access("hasRole('ROLE_USER')")
                .antMatchers("/", "/**").access("permitAll")

            .and()
                .formLogin()
                .loginPage("/login");
    }

  请注意,在调用formLogin()之前,我们通过and()方法将这一部分的配置与前面的配置连接在一起。and()方法表示我们已经完成了授权相关的配置,并且要添加一些其他的HTTP配置。在开始新的配置区域时,我们可以多次调用and()。

  在这个连接之后,我们调用formLogin()开始配置自定义的登录表单。在此之后,对loginPage()的调用声明了我们提供的自定义登录页面的路径。当Spring Security断定用户没有认证并且需要登录的时候,它就会将用户重定向到该路径。

  默认情况下,Spring Security会在“/login”路径监听登录请求并获取用户名为username和密码为password的请求参数,但这些都是可配置的。举例来说,如下的配置自定义了路径和请求参数的名称:

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .authorizeRequests()
                .antMatchers("/design", "/orders")
                    .access("hasRole('ROLE_USER')")
                .antMatchers("/", "/**").access("permitAll")

            .and()
                .formLogin()
                .loginPage("/login")
                .loginProcessingUrl("/authenticate")
                .usernameParameter("user")
                .passwordParameter("pwd");
    }

  在这里,我们指定 Spring Security 应该监听对 /authenticate 的请求以处理登录信息的提交。此外,用户名和密码字段已被指定为 user 和 pwd。

  默认情况下,登录成功之后,用户将会被导航到Spring Security决定让用户登录之前的页面。如果用户直接访问登录页,那么登录成功之后用户将会被导航至根目录(例如,主页)。但是,我们可以通过指定默认的成功页来更改这种行为:

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .authorizeRequests()
                .antMatchers("/design", "/orders")
                    .access("hasRole('ROLE_USER')")
                .antMatchers("/", "/**").access("permitAll")

            .and()
                .formLogin()
                .loginPage("/login")
                .defaultSuccessUrl("/design");
    }

  按照这个配置,用户直接导航至登录页并且成功登录之后将会被定向到"/design"页面。

  另外,我们还可以强制用户在登录成功之后统一访问设计页面,即便用户在登陆之前正在访问其他页面,在登录之后也会被定向到设计页面,这可以通过为defaultSuccessUrl方法传递第二个参数true来实现:

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .authorizeRequests()
                .antMatchers("/design", "/orders")
                    .access("hasRole('ROLE_USER')")
                .antMatchers("/", "/**").access("permitAll")

            .and()
                .formLogin()
                .loginPage("/login")
                .defaultSuccessUrl("/design", true);
    }

  3.退出登录

  退出和登录是同等重要的,为了启用退出功能,我们只需在HttpSecurity对象上调用logout方法:

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .authorizeRequests()
                .antMatchers("/design", "/orders")
                    .access("hasRole('ROLE_USER')")
                .antMatchers("/", "/**").access("permitAll")

            .and()
                .formLogin()
                .loginPage("/login")
                .defaultSuccessUrl("/design", true)
            .and()
                .logout()
                .logoutSuccessUrl("/");
    }

  这样会搭建一个安全过滤器,该过滤器会拦截对“/logout”的请求。所以,为了提供退出功能,我们需要为应用的视图添加一个退出表单和按钮。

<form method="POST" th:action="@{/logout}">
    <input type="submit" value="Logout"/>
</form>

  当用户点击按钮的时候,他们的session将会被清理,这样他们就退出应用了。默认情况下,用户会被重定向到登录页面,这样他们可以重新登录。但是,如果你想要将他们导航到不同的页面,那么可以调用logoutSuccessUrl()指定退出后的不同页面

  4.防止跨站请求伪造

  跨站请求伪造(Cross-Site Request Forgery,CSRF)是一种常见的安全攻击。它会让用户在一个恶意设计的 web 页面上填写信息,这个页面会自动(通常是秘密地)将表单以攻击受害者的身份提交到另外一个应用上。例如,用户看到一个来自攻击者的Web站点的表单,这个站点会自动将数据POST到用户银行Web站点的URL上(这个站点可能设计的很糟糕,无法防御这种类型的攻击),实现转账的操作。用户可能根本不知道发生了攻击,直到他们发现账号上的钱已经不翼而飞。

  为了防止这种类型的攻击,应用可以在展现表单的时候生成一个 CSRF token,并将该 token 放在隐藏域中,然后将其临时存储起来,以便后续在服务器上使用。在提交表单的时候,token 将和表单的其他数据一起发送至服务器。然后服务器拦截请求,并将其与最初生成的 token 进行对比。如果 token 匹配,则允许继续执行请求。否则,表单肯定是由恶意网站渲染的,因为它不知道服务器所生成的token。

  幸运的是,Spring Security 有内置的 CSRF 保护。更幸运的是,它是默认启用的,不需要显式地配置它。我们唯一需要做的就是确保应用中的每个表单都要有一个名为 _csrf 的字段,该字段包含 CSRF token。

  Spring Security 甚至进一步简化了将token放到请求的 “_csrf ”属性中这一任务。在 Thymeleaf 模板中,我们可以按照如下的方式在隐藏域中渲染 CSRF token:

<input type="hidden" name="_csrf" th:value="${_csrf.token}"/>

  如果你使用 Spring MVC 的 JSP 标签库或者Spring Security的 Thymeleaf 方言,那么甚至都不用明确包含这个隐藏域(这个隐藏域会自动生成)

  在 Thymeleaf 中,只需确保 <form> 的某个属性带有 Thymeleaf 属性前缀即可。通常这并不是什么问题,因为我们一般会使用 Thymeleaf 渲染相对于上下文的路径。例如,为了让Thymeleaf 渲染隐藏域,我们只需要使用 th:action 属性就可以了:

<form method="POST" th:action="@{/login}" id="loginForm">

  我们也可以禁用 CSRF 支持,CSRF 防护很重要,而且在表单中很容易实现,所以没有理由禁用它,但如果你坚持禁用它,那么可以通过调用 disable()来实现:

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .authorizeRequests()
                .antMatchers("/design", "/orders")
                    .access("hasRole('ROLE_USER')")
                .antMatchers("/", "/**").access("permitAll")

            .and()
                .formLogin()
                .loginPage("/login")
                .defaultSuccessUrl("/design", true)
            .and()
                .logout()
                .logoutSuccessUrl("/")
            .and()
                .csrf()
                .disable();
    }

  再次强调,不要禁用 CSRF 保护,特别是对于生产环境中的应用程序。

⒋获取系统中已登录的用户信息

  通常,仅仅知道用户已登录是不够的,我们一般还需要知道他们是谁,这样才能优化体验。

  我们有多种方式确定用户是谁,常用的方式如下:

  • 注入Principal对象到控制器方法中
  • 注入Authentication对象到控制器方法中
  • 使用@AuthenticationPrincipal注解来标注方法
  • 使用SecurityContextHolder来获取安全上下文

  1.注入Principal对象到控制器方法中

    @PostMapping
    public User getLoginUser(Principal principal){
        User user = userRepository.findByUsername(principal.getName());
        return user;
    }

  这种方式能够正常运行,但是它会在与安全无关的功能中掺杂安全性的代码,我们可以修改这个方法,让它不再接受Principal参数,而是接受Authentication对象作为参数:

  2.注入Authentication对象到控制器方法中

    @PostMapping
    public User getLoginUser(Authentication authentication){
        User user = null;
        if(authentication.getPrincipal() instanceof  User){
            user = (User) authentication.getPrincipal();
        }
        return user;
    }

  有了Authentication对象之后,我们就可以调用getPrincipal()来获取principal对象,在本例中,也就是一个User对象。需要注意,getPrincipal()返回的是java.util.Object,所以我们需要将其转换成User。

 

  3.使用@AuthenticationPrincipal注解来标注方法

  最整洁的方案可能是在该方法中直接接受一个User对象,不过我们需要为其添加@AuthenticationPrincipal注解,这样它才会变成认证的principal。

    @PostMapping
    public User getLoginUser(@AuthenticationPrincipal User user){
        return user;
    }

  @AuthenticationPrincipal非常好的一点在于它不需要类型转换(上面的Authentication则需要进行类型转换),同时能够将安全相关的代码仅仅局限于注解本身。

  

  4.使用SecurityContextHolder来获取安全上下文

  还有另外一种方式能够识别当前认证用户是谁,但是这种方式有点麻烦,它会包含大量安全性相关的代码。我们可以从安全上下文中获取一个Authentication对象,然后像下面这样获取它的principal:

    @PostMapping
    public User getLoginUser(){
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        User user = (User)authentication.getPrincipal();
        return user;
    }

  尽管这个代码片段充满了安全性相关的代码,但是它与前面的其他方法相比有一个优势:它可以在应用的任何地方使用,而不仅仅是在控制器的处理器方法中,这使得它非常适合在较低级别的代码中使用。

 

  

posted @ 2020-06-12 15:56  SpringCore  阅读(441)  评论(0编辑  收藏  举报