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" ;
}
}
<! 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:/" ;
}
}