第4章保护Spring

Spring实战

第4章 保护Spring

启用 Spring Security

  • 保护Spring应用的第一步就是将Spring Boot Security starter依赖添加到构建文件中
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>
  • 可以尝试一下,启动应用并尝试访问任意页面。应用将会弹出一个HTTP basic认证对话框并提示进行认证。要想通过这个认证,需要一个用户名和密码。用户名为user,密码是随机生成的,会被写入应用的日志文件中。
Using generated security password: 830b144e-f507-4059-9761-a83a0c2abff9
  • 输入正确的用户名和密码之后,就有权进行访问了。
  • 通过将Security starter添加到项目的构建文件中,我们得到了如下的安全特性:
    • 所有的HTTP请求路径都需要认证
    • 不需要特定的角色和权限
    • 没有登录页面
    • 认证过程是通过一个HTTP basic认证对话框实现的
    • 系统只有一个用户,用户名为user
  • 下面,我们将为了实现需要的功能,配置Spring Security

配置 Spring Security

  • 有很多配置方式,比如冗长的XML的配置。但是最近版本的Spring Security支持基于Java的配置,这种方式更易于阅读和编写。
  • 下面是Spring Security的基础配置类
package tacos.security;

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 {
}
  • 注意继承的是xxxAdapter类,懂的都懂。
  • Spring Security为配置用户存储提供了多个可选方案:
    • 基于内存的用户存储
    • 基于JDBC的用户存储
    • 以LDAP作为后端的用户存储
    • 自定义用户详情服务
  • 不管使用哪种用户存储,都可以通过覆盖WebSecurityConfigurerAdapter基础配置类中定义的configure()方法来进行配置。首先,可以将下面的方法添加到SecurityConfig类中。
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    ...;
}
基于内存的用户存储
  • 用户信息可以存储在内存中。假设只有数量有限的几个用户且几乎不会发生变化,在这种情况下,将这些用户定义成安全配置的是非常简单的。
  • 下面的程序将在内存用户存储中配置两个用户“buzz”和“woody”
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth
            .inMemoryAuthentication()
                .withUser("buzz")
                    .password("{noop}infinity")
                    .authorities("ROLE_USER")
                .and()
                .withUser("woody")
                    .password("{noop}bullseye")
                    .authorities("ROLE_USER");
    }
  • 需要注意的是,使用了inMemoryAuthentication()方法来指定用户信息,也就是配置在了内存中
基于JDBC的用户存储
  • 用户信息通常会在关系型数据库中进行维护,基于JDBC的用户存储方案会更加合理一些。下面的程序展示了使用JDBC对存储在关系型数据库中的用户信息进行认证所需的Spring Security配置。
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth
                .jdbcAuthentication()
                .dataSource(dataSource);
    }
    
    @Autowired
    DataSource dataSource;
  • 在这里的configure实现中,调用了jdbcAuthentication方法。我们还必须要配置一个DataSource,这样它才能知道如何访问数据库。这里的DataSource是通过自动装配的技巧获取到的。
重写默认的用户查询功能
  • 默认的用户查询中,获取用户的用户名、密码以及是否启用的信息,用来进行用户认证。但是,可能我们的数据库与默认的不一致,那么可能会希望在查询上有更多控制权,下面是配置自己的查询:
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth
                .jdbcAuthentication()
                .dataSource(dataSource)
                .usersByUsernameQuery(
                        "select username, password, enabled from Users " +
                        "where username=?"
                )
                .authoritiesByUsernameQuery(
                        "select username, authority from UserAuthorities " + 
                        "where username=?"
                );
    }

    @Autowired
    DataSource dataSource;
  • 在本例中,只重写了认证和基本权限的查询语句,但是通过调用groupAuthoritiesByUsername()方法,我们也能够将群组权限重写为自定义查询语句。
  • 将默认的SQL查询替换为自定义的设计时,很重要一点就是要遵循查询基本协议。所有查询都会讲用户名作为唯一参数。认证查询会选取用户名、密码以及启用状态信息。权限查询会选取零行或多行包含该用户名及其权限信息的数据。群组权限查询会选取零行或多行数据,每行数据中都会包含群组ID、群组名称以及权限。
使用转码后的密码
  • 看上面的认证查询,预期用户密码存储到了数据库。这里唯一的问题是如果密码使用明文存储,很容易收到黑客攻击。但是,如果数据库中的密码进行了转码,那么认证就会失败,因为它与用户提交的明文密码并不匹配。
  • 为解决这个问题,我们需要借助passwordEncoder()方法指定一个密码转码器:
package tacos.security;

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.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.password.Pbkdf2PasswordEncoder;

import javax.sql.DataSource;

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth
                .jdbcAuthentication()
                .dataSource(dataSource)
                .usersByUsernameQuery(
                        "select username, password, enabled from Users " +
                        "where username=?"
                )
                .authoritiesByUsernameQuery(
                        "select username, authority from UserAuthorities " +
                        "where username=?"
                )
                .passwordEncoder(new Pbkdf2PasswordEncoder("123456"));
    }

    @Autowired
    DataSource dataSource;
}
  • 上面代码使用了Pbkdf2PasswordEncoder,也就是使用了PBKDF2进行加密。除此之外,还有很多种加密方式,甚至可以实现PasswordEncoder接口中的encode和match两个方法进行实现。
以LDAP作为后端的用户存储
  • 为了配置Spring Security使用基于LDAP认证,可以使用公ldapAuthentication()方法。这个方法在功能上类似于jdbcAuthentication(),只不过是LDAP版本。如下的configure()方法展现了LDAP认证的简单配置
package tacos.security;

import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
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 {
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth
                .ldapAuthentication()
                .userSearchBase("ou=people")
                .userSearchFilter("(uid={0})")
                .groupSearchBase("ou=groups")
                .groupSearchFilter("member={0}");
    }
}
  • 方法userSearchFilter()和groupSearchFilter()用来为基础LDAP查询提供过滤条件,分别用于搜索用户和组。默认情况下,对于用户和组的基础查询都是空的,也就是表明搜索会在LADP层级结构的根开始。但是我们可以通过指定查询基础来改变这个默认行为。这样的话,用户应该在名为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");
}
引用远程的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");
}
配置嵌入式的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数据的标准方式。每条记录可以有一行或多行,每项包含一个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");
}
自定义用户认证
定义用户领域对象和持久化
  • 当Taco Cloud的顾客注册应用的时候,需要提供除了用户名和密码以外的更多信息。它们会提供全名、地址和电话号码。这些信息可以用于各种目的,包括预先填充表单。
  • 为了捕获这些信息,我们要创建下面的User类。
package tacos;

import lombok.AccessLevel;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import java.util.Arrays;
import java.util.Collection;

@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;
    }
}
  • User类比前面定义的实体都更加复杂,除了定义了一些属性之外,User类还实现了Spring Security的UserDetails接口。
  • 通过实现该接口,可以提供更多信息给框架,比如用户都被授予了哪些权限以及用户的账号是否可用。
  • getAuthorities()方法应该返回用户被授予权限的一个集合。各种is…Expired()方法都返回一个布尔值,表明用户的账号是否可用或已经过期。
  • 下面是定义repository接口
package tacos.data;

import org.springframework.data.repository.CrudRepository;
import tacos.User;

public interface UserRepository extends CrudRepository<User, Long> {
    User findByUsername(String username);
}
  • 除了扩展CrudRepository所得到的CRUD操作之外,UserRepository接口还定义了一个findByUsername方法
创建用户详情服务
  • Spring Security的UserDetialsService是一个相当简单的接口:
package org.springframework.security.core.userdetails;

public interface UserDetailsService {
    UserDetails loadUserByUsername(String var1) throws UsernameNotFoundException;
}
  • 这个接口的实现将会得到一个用户的用户名,并且要么返回查找到的UserDetials对象,要么在根据用户名无法得到任何结果的情况下抛出UsernameNotFoundException。
  • 因为User类实现了UserDetails,并且UserRepository提供了findByUsername()方法,所以它们非常适合用在UserDetailsService实现中。
package tacos.security;

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 tacos.User;
import tacos.data.UserRepository;

@Service
public class UserRepositoryUserDetailsService implements UserDetailsService {
    
    private UserRepository userRepo;
    
    @Autowired
    public UserRepositoryUserDetailsService(UserRepository userRepo)
    {
        this.userRepo = userRepo;
    }

    @Override
    public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
        User user = userRepo.findByUsername(s);
        if(user != null)
        {
            return user;
        }
        throw new UsernameNotFoundException(
                "User '" + s + "' not found"
        );
    }
}
  • 该类通过构造器将UserRepository注入进来。并在loadUserByUsername方法中调用其findByUsername方法来查找User。
  • 该类添加了注解@Service,这是Spring另一个构造型注解,表明这个类要包含到Spring的组件扫描中,Spring会自动发现他并将其初始化为一个bean。
  • 但是,我们依然需要将这个自定义的用户详情服务与Spring Security配置在一起。因此,还需要回到configure()方法
package tacos.security;

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.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UserDetailsService;


@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth
                .userDetailsService(userDetailsService);
    }
    
    @Autowired
    private UserDetailsService userDetailsService;
}
  • 在这里,只是简单地调用了userDetailsService()方法,并将自动装配到SecurityConfig中的UserDetailsService实例传递了进去。
  • 像基于JDBC的认证一样,我们也应该配置一个密码转码器。
package tacos.security;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
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.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.crypto.password.Pbkdf2PasswordEncoder;


@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth
                .userDetailsService(userDetailsService)
                .passwordEncoder(encoder());
    }

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

    @Bean
    public PasswordEncoder encoder(){
        return new Pbkdf2PasswordEncoder("123456");
    }
}
注册用户
  • 尽管在安全性方面,Spring Security会为我们处理很多事,但是它没有直接涉及用户注册的流程,所以我们需要借助Spring MVC的一些技能来完成这个任务。下面的类会负责展现和处理注册表单。
package tacos.security;

import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import tacos.data.UserRepository;

@Controller
@RequestMapping("/register")
public class RegistrationController {
    private UserRepository userRepo;
    private PasswordEncoder passwordEncoder;

    public RegistrationController(UserRepository userRepo, PasswordEncoder passwordEncoder)
    {
        this.passwordEncoder = passwordEncoder;
        this.userRepo = userRepo;
    }

    @GetMapping
    public String registerForm()
    {
        return "registration";
    }

    @PostMapping
    public String processRegistration (RegistrationForm form)
    {
        userRepo.save(form.toUser(passwordEncoder));
        return "redirect:/login";
    }
}
  • 下面是注册表单的Thymeleaf视图
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Taco Cloud</title>
</head>
<body>
    <h1>Register</h1>
    <img th:src="@{/images/TacoCloud.png}"/>
    <form method="post" th:action="@{register}" id="registerForm">

        <label for="username">Username: </label>
        <input type="text" name="username"/><br/>

        <label for="password">Password: </label>
        <input type="password" name="password"/><br/>

        <label for="confirm">Confirm password: </label>
        <input type="password" name="confirm"/><br/>

        <label for="fullname">Full name: </label>
        <input type="text" name="fullname"/><br/>

        <label for="street">Street: </label>
        <input type="text" name="street"/><br/>

        <label for="city">City: </label>
        <input type="text" name="city"/><br/>

        <label for="state">State: </label>
        <input type="text" name="state"/><br/>

        <label for="zip">Zip: </label>
        <input type="text" name="zip"/><br/>

        <label for="phone">Phone: </label>
        <input type="text" name="phone"/><br/>

        <input type="submit" value="Register"/>
    </form>

</body>
</html>
  • 当表单提交时,processRegistration()方法会处理HTTP POST请求。ProcessRegistration()方法得到RegistrationForm对象绑定了请求的数据,该类定义如下:
package tacos.security;

import lombok.Data;
import org.springframework.security.crypto.password.PasswordEncoder;
import tacos.User;

@Data
public class RegistrationForm {
    private String username;
    private String password;
    private String fullname;
    private String street;
    private String city;
    private String state;
    private String zip;
    private String phone;

    public User toUser(PasswordEncoder passwordEncoder)
    {
        return new User(
                username, passwordEncoder.encode(password),
                fullname, street, city, state, zip, phone
        );
    }
}
  • toUser()方法使用这些属性创建了一个新的User对象,processRegistration()使用注入的UserRepository保存了该对象。
  • 可以发现RegistrationController注入了一个PasswordEncoder,在密码保存到数据库前,对其进行转码。
  • 现在Taco Cloud应用已经有了完整的用户注册和认证功能。但是如果现在启动应用会发现无法进入注册页面,也不会提示登录。这是因为默认情况下,所有的请求都需要认证。

保护 Web 请求

  • Taco Cloud的安全性需求是在用户在设计taco和提交订单时必须要经过认证。但是,主页、登录页、以及注册页应该对未认证的用户开放。
  • 为了配置这些安全性规则,需要了解configure()功能
    • 在为某个请求提供服务前,需要预先满足特定条件
    • 配置自定义登录页
    • 支持用户退出应用
    • 预防跨站请求伪造
  • 配置HttpSecurity常见的需求就是拦截请求以确保用户具备适当的权限。
保护请求
  • 我们需要确保只有认证过的用户才能发起对"/design"和"/orders"的请求,而其他请求对所有用户都可用。
@Override
protected void configure(HttpSecurity http) throws Exception {
    http
        .authorizeRequests()
        .antMatchers("/design", "/orders")
        .hasRole("ROLE_USER")
        .antMatchers("/", "/**")
        .permitAll()
}
  • 对authorizeRequests()的调用会返回一个对象,基于它我们可以指定URL路径和这些路径的安全需求。在本例中,我们指定了两条安全规则。
    • 具备ROLE_USER权限的用户才可以访问"/design"和"/orders"
    • 其他的请求允许所有用户访问
  • 但是注意不能交换两个安全规则的顺序。
  • 安全规则的写法有很多种,例如我们可以将上面程序重写为
@Override
protected void configure(HttpSecurity http) throws Exception {
    http
        .authorizeRequests()
        .antMatchers("/design", "/orders")
        .access("hasRole('ROLE_USER')")
        .antMatchers("/", "/**")
        .access("permitAll()");
}
创建自定义的登录页
  • 默认登录页已经比最初丑陋的HTTP basic认证对话框好了很多,但是依然十分简单。
  • 为了替换内置登录页,我们首先需要告诉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")
}
  • and()表明开始一个新的配置区域。
  • 当Spring Security断定用户没有认证并且需要登录的时候,就会将用户重定向到该路径。
  • 现在,我们需要一个控制器来处理对该路径的请求。因为我们的登录页非常简单,只有一个视图,没有其他内容,所以可以很简单的在WebConfig中将其声明为一个视图控制器。
package tacos.web;

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class WebConfig implements WebMvcConfigurer {
    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
        registry.addViewController("/").setViewName("home");
        registry.addViewController("/login");
    }
}
  • 最后我们需要自己定义登录页视图
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:th="http://www.thymeleaf.org">
<head>
    <title>Taco Cloud</title>
</head>

<body>
<h1>Login</h1>
<img th:src="@{/images/TacoCloud.png}"/>

<div th:if="${error}">
    Unable to login. Check your username and password.
</div>

<p>New here? Click
    <a th:href="@{/register}">here</a> to register.</p>

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

    <label for="username">Username: </label>
    <input type="text" name="username" id="username" /><br/>

    <label for="password">Password: </label>
    <input type="password" name="password" id="password" /><br/>

    <input type="submit" value="Login"/>
</form>
</body>
</html>
  • 该登录页需关注的是表单提交到哪里以及用户名、密码输入域的名称。默认情况下,Spring Security会在"/login"路径监听登录请求并且预期的用户名和密码输入域的名称为username和password。这都是可配置的。
退出
  • 为了启用退出功能,我们只需要在HttpSecurity对象上调用logout方法。
.and()
    .logout()
    .logoutSuccessUrl("/");
  • 这样会搭载一个安全过滤器,该过滤器会拦截对"/logout"的请求。所以,为了提供退出功能,我们需要为应用的视图添加一个退出表单和按钮。
<form method="post" th:action="@{/logout}" id="logoutForm">
    <input type="submit" value="Logout">
</form>
防止跨站请求伪造
  • 在Thymeleaf模板中。我们可以按照如下的方式在隐藏域中渲染CSRF token:
<input type="hidden" name="_csrf" th:value="${_csrf.token}"/>
  • 但是如果使用了Spring Security的Thymeleaf方言,那么该隐藏域会自动生成。
  • 为了让Thymeleaf渲染隐藏域,我们只需要使用th:action属性就可以了。
<form method="POST" th:action="@{/login}" id="loginForm">

了解用户是谁

  • 通常,仅仅知道用户已登录是不够的,我们一般还需要知道他的身份,以优化用户体验。
  • 例如,在OrderController中,在最初创建Order的时候会绑定一个订单的表单,如果我们能够预先将用户的姓名和地址填充到Order中就好了,这样用户就不需要为每个订单都重新输入这些信息了。也许更重要的是,在保存订单的时候应该将Order实体与创建该订单的用户关联起来。
  • 为了让Order实体与User实体之间实现所需的关联,我们需要为Order类添加一个新的属性:
@ManyToOne
private User user;
  • 这个属性上的@ManyToOne注解表明了一个订单只能属于一个用户,但是,一个用户却可以有多个订单。因为使用了Lombok,所以不需要为该属性显式定义访问器方法。
  • 在OrderController中,processOrder()方法负责保存订单。这个方法需要修改以便于确定当前的认证用户是谁,并要调用Order对象的setUser()方法来建立订单和用户之间的关联。
  • 我们有多种方式可以确定用户是谁,常见方式如下:
    • 注入Principal对象到控制器方法中;
    • 注入Authentication对象到控制器方法中;
    • 使用SecurityContextHolder来获取安全上下文;
    • 使用@AuthenticationPrincipal注解来标注方法;
  • 我们推荐使用在processOrder()中直接添加一个接受的User对象,不过需要为其添加@AuthenticationPrincipal注解,这样他才会变成认证的principal:
package tacos.web;

import lombok.extern.slf4j.Slf4j;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.stereotype.Controller;
import org.springframework.validation.Errors;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.SessionAttributes;
import org.springframework.web.bind.support.SessionStatus;
import tacos.Order;
import tacos.User;
import tacos.data.OrderRepository;
import tacos.data.UserRepository;

import javax.validation.Valid;
import java.security.Principal;


@Controller
@RequestMapping("/orders")
@SessionAttributes("order")
public class OrderController {
    private OrderRepository orderRepo;

    public OrderController(OrderRepository orderRepo)
    {
        this.orderRepo = orderRepo;
    }

    @GetMapping("/current")
    public String orderForm()
    {
        return "orderForm";
    }

    @PostMapping
    public String processOrder(@Valid Order order, Errors errors, SessionStatus sessionStatus, @AuthenticationPrincipal User user)
    {
        if(errors.hasErrors())
            return "orderForm";

        order.setUser(user);

        orderRepo.save(order);
        sessionStatus.setComplete();
        return "redirect:/";
    }
}
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Lanciberrr

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值