写在前面

前一篇学习了如何实现自动登录,但是随之而来的是以牺牲系统安全为代价,这一点在很多场景下都是不可取的,因此就必须对自动登录的核心—令牌进行一些安全提升,或者采用二次验证等方式来提升系统的安全性。

令牌持久化

概念

前面提到过一个makeTokenSignature()方法,该方法的逻辑是计算令牌过期时间、用户名、密码和盐Key参数所构成字符串的哈希值:

1
2
3
4
5
6
7
8
9
10
protected String makeTokenSignature(long tokenExpiryTime, String username, String password) {
String data = username + ":" + tokenExpiryTime + ":" + password + ":" + this.getKey();

try {
MessageDigest digest = MessageDigest.getInstance("MD5");
return new String(Hex.encode(digest.digest(data.getBytes())));
} catch (NoSuchAlgorithmException var7) {
throw new IllegalStateException("No MD5 algorithm available!");
}
}

可以将其理解为是tokenValue,前面说过这个tokenValue默认存储在cookie中,这是非常不安全的,因此可以将其持久化到数据库中,同时添加新的校验参数,这样极大的提升了系统的安全性,而且对用户体验没有任何影响。

也就是说所谓的令牌持久化,其实就是在基本的自动登录功能上添加了新的校验参数而已,这样可提高系统的安全性。

在持久化令牌中,我们新增了两个经过MD5散列函数计算的校验参数:series和token。其中series是仅当用户在使用用户名和密码登录时,才会生成或者更新;而token则是只要有新的会话,它就会重新生成,这样做的好处就是可以避免一个用户同时在多端登录。以手机QQ为例,当用户在A手机登录,之后在B手机登录就会踢掉之前在A上的登录,这样可以发现账号是否泄漏,这也是禁止多端登录的一个比较通用做法。

通过前一篇《实现自动登录》一文的学习,我们知道自动化登录的具体处理类为TokenBasedRememberMeServices,而我们持久化令牌使用到的处理类为PersistentTokenBasedRememberMeServices,这个类怎么找到的呢?

首先通过TokenBasedRememberMeServices类知道它继承自抽象类AbstractRememberMeServices,之后通过查看这个抽象类的具体实现类就发现了这个PersistentTokenBasedRememberMeServices类,三者的继承关系如下所示:

接下来查看一下这个PersistentTokenBasedRememberMeServices类的源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
public class PersistentTokenBasedRememberMeServices extends AbstractRememberMeServices {
private PersistentTokenRepository tokenRepository = new InMemoryTokenRepositoryImpl();
private SecureRandom random = new SecureRandom();
public static final int DEFAULT_SERIES_LENGTH = 16;
public static final int DEFAULT_TOKEN_LENGTH = 16;
private int seriesLength = 16;
private int tokenLength = 16;

public PersistentTokenBasedRememberMeServices(String key, UserDetailsService userDetailsService, PersistentTokenRepository tokenRepository) {
//逻辑
}

protected UserDetails processAutoLoginCookie(String[] cookieTokens, HttpServletRequest request, HttpServletResponse response) {
if (cookieTokens.length != 2) {
throw new InvalidCookieException("Cookie token did not contain 2 tokens, but contained '" + Arrays.asList(cookieTokens) + "'");
} else {
String presentedSeries = cookieTokens[0];
String presentedToken = cookieTokens[1];
PersistentRememberMeToken token = this.tokenRepository.getTokenForSeries(presentedSeries);
if (token == null) {
throw new RememberMeAuthenticationException("No persistent token found for series id: " + presentedSeries);
} else if (!presentedToken.equals(token.getTokenValue())) {
this.tokenRepository.removeUserTokens(token.getUsername());
throw new CookieTheftException(this.messages.getMessage("PersistentTokenBasedRememberMeServices.cookieStolen", "Invalid remember-me token (Series/token) mismatch. Implies previous cookie theft attack."));
} else if (token.getDate().getTime() + (long)this.getTokenValiditySeconds() * 1000L < System.currentTimeMillis()) {
throw new RememberMeAuthenticationException("Remember-me login has expired");
} else {
this.logger.debug(LogMessage.format("Refreshing persistent login token for user '%s', series '%s'", token.getUsername(), token.getSeries()));
PersistentRememberMeToken newToken = new PersistentRememberMeToken(token.getUsername(), token.getSeries(), this.generateTokenData(), new Date());

try {
this.tokenRepository.updateToken(newToken.getSeries(), newToken.getTokenValue(), newToken.getDate());
this.addCookie(newToken, request, response);
} catch (Exception var9) {
this.logger.error("Failed to update token: ", var9);
throw new RememberMeAuthenticationException("Autologin failed due to data access problem");
}

return this.getUserDetailsService().loadUserByUsername(token.getUsername());
}
}
}

protected void onLoginSuccess(HttpServletRequest request, HttpServletResponse response, Authentication successfulAuthentication) {
//逻辑
}

public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) {
//逻辑
}

protected String generateSeriesData() {
//逻辑
}

protected String generateTokenData() {
//逻辑
}

private void addCookie(PersistentRememberMeToken token, HttpServletRequest request, HttpServletResponse response) {
//逻辑
}

public void setSeriesLength(int seriesLength) {
//逻辑
}

public void setTokenLength(int tokenLength) {
//逻辑
}
public void setTokenValiditySeconds(int tokenValiditySeconds) {
//逻辑
}

这里将除了processAutoLoginCookie方法外的其余方法的具体逻辑给抹除了,仔细看这个processAutoLoginCookie方法的源码,可以发现它有一个PersistentRememberMeToken对象,查看一下这个对象的源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class PersistentRememberMeToken {
private final String username;
private final String series;
private final String tokenValue;
private final Date date;

public PersistentRememberMeToken(String username, String series, String tokenValue, Date date) {
this.username = username;
this.series = series;
this.tokenValue = tokenValue;
this.date = date;
}
//getter和setter方法
}

可以发现这个类中除了用户名,还有series、tokenValue和date(上一次使用自动登录的时间)等属性,这就是持久化令牌时的实体类,后续会使用到这个类。

代码示例

接下来通过代码来学习持久化令牌的相关内容。

我们需要将令牌持久化到数据库中,因此需要定义一张表来记录令牌信息,这个表中的字段可以使用系统默认的字段,这就是上面所说的PersistentRememberMeToken类中的属性,当然也可以自定义。那么如何自定义呢?在此之前我们需要先分析系统是如何使用JDBC来操作默认字段的。

还是回到之前的PersistentTokenBasedRememberMeServices#processAutoLoginCookie()方法中,可以看到在获取PersistentRememberMeToken对象时,使用的代码如下:

1
PersistentRememberMeToken token = this.tokenRepository.getTokenForSeries(presentedSeries);

如果你使用过JDBC或者JPA那么知道这里的tokenRepository就是数据库提供的操作对象,点进去查看它的源码,可以看到它跳到如下位置:

1
private PersistentTokenRepository tokenRepository = new InMemoryTokenRepositoryImpl();

显然左侧的PersistentTokenRepository是一个接口,右侧的InMemoryTokenRepositoryImpl就是该接口的一个实现类,且是基于内存的Token操作,而这里我们学习的是数据库,因此猜测这个PersistentTokenRepository接口应该有一个用于数据库操作的实现类,查看一下,可以发现确实存在JdbcTokenRepositoryImpl这个实现类。源码看多了,这种其实很容易就能猜的出来。查看一下这个JdbcTokenRepositoryImpl的源码,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
public class JdbcTokenRepositoryImpl extends JdbcDaoSupport implements PersistentTokenRepository {
public static final String CREATE_TABLE_SQL = "create table persistent_logins (username varchar(64) not null, series varchar(64) primary key, token varchar(64) not null, last_used timestamp not null)";
public static final String DEF_TOKEN_BY_SERIES_SQL = "select username,series,token,last_used from persistent_logins where series = ?";
public static final String DEF_INSERT_TOKEN_SQL = "insert into persistent_logins (username, series, token, last_used) values(?,?,?,?)";
public static final String DEF_UPDATE_TOKEN_SQL = "update persistent_logins set token = ?, last_used = ? where series = ?";
public static final String DEF_REMOVE_USER_TOKENS_SQL = "delete from persistent_logins where username = ?";
private String tokensBySeriesSql = "select username,series,token,last_used from persistent_logins where series = ?";
private String insertTokenSql = "insert into persistent_logins (username, series, token, last_used) values(?,?,?,?)";
private String updateTokenSql = "update persistent_logins set token = ?, last_used = ? where series = ?";
private String removeUserTokensSql = "delete from persistent_logins where username = ?";
private boolean createTableOnStartup;
......
}

是不是感觉这个页面非常熟悉?是的,在前面学习数据库认证的时候就是这样,可以看到这里面定义的都是一些SQL语句,而通过语句中的内容开发者就能分析出系统默认表的结构。

结合笔者分析出的信息及自定义SQL语句等,可以得到我们所需的数据表:

1
2
3
4
5
6
7
8
use envysecurity;
drop table if exists persistent_logins;
create table persistent_logins (
username varchar(64) not null,
series varchar(64) primary key,
token varchar(64) not null,
last_used timestamp not null
)engine=INNODB default charset=utf8;

由于需要将数据存到数据库中,因此需要在项目的pom.xml依赖文件中新增如下依赖:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>

<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>

<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>

接着修改application.yml文件中的内容为如下所示:

1
2
3
4
5
6
7
8
9
10
spring:
security:
user:
name: "envythink"
password: "1234"
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql:///envysecurity?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
username: root
password: root

然后修改MySecurityConfig类的信息为如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
@Configuration
public class MySecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private MyUserDetailsService myUserDetailsService;

@Autowired
private DataSource dataSource;

@Bean
PasswordEncoder passwordEncoder(){
return NoOpPasswordEncoder.getInstance();
}

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

@Bean
JdbcTokenRepositoryImpl jdbcTokenRepository(){
JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl();
jdbcTokenRepository.setDataSource(dataSource);
return jdbcTokenRepository;
}

@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.anyRequest().authenticated()
.and()
.formLogin()
.and()
.rememberMe()
.key("envy")
.tokenRepository(jdbcTokenRepository())
.and()
.csrf().disable();
}
}

可以看到我们往MySecurityConfig类中新增了如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Autowired
private DataSource dataSource;

@Bean
JdbcTokenRepositoryImpl jdbcTokenRepository(){
JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl();
jdbcTokenRepository.setDataSource(dataSource);
return jdbcTokenRepository;
}

@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.anyRequest().authenticated()
.and()
.formLogin()
.and()
.rememberMe()
.key("envy")
.tokenRepository(jdbcTokenRepository())
.and()
.csrf().disable();
}

可以看到我们首先注入一个DataSource对象,然后提供一个JdbcTokenRepositoryImpl实例,并将注入的DataSource对象作为数据源赋值给JdbcTokenRepositoryImpl实例,接着通过调用.tokenRepository(jdbcTokenRepository())方法传入PersistentTokenRepository对象,这样我们就完成了持久化的配置工作。

项目测试

接下来启动项目,然后访问http://localhost:8080/hello接口,此时页面会自动跳转到登录页面,之后我们输入用户名和密码,并勾选“记住我”选框,登录成功后,系统就显示“Hello,World!”,这就说明我们上面配置的持久化令牌就已经生效了。

查看一下此时的令牌信息,如下所示:

也就是说此时令牌信息为:

1
JTJGc1YlMkZoRjNGWjZDMWV1NGRkYXp2alElM0QlM0Q6UmUyQU9vQmg0c1Zqa1hwTzJXJTJGY05BJTNEJTNE

将这个令牌使用MD5进行解密,发现解密后的信息如下所示:

1
%2FsV%2FhF3FZ6C1eu4ddazvjQ%3D%3D:Re2AOoBh4sVjkXpO2W%2FcNA%3D%3D

请注意其中的%2F表示/%3D表示=,因此上面的令牌其实就是如下的字符串:

1
/sV/hF3FZ6C1eu4ddazvjQ==:Re2AOoBh4sVjkXpO2W/cNA==

接着查看一下数据库,可以发现此时数据中已经生成了一条记录:

可以看到此时数据库中的记录和之前我们通过解析得到的RememberMe令牌信息是一致的,也就说明我们令牌持久化的配置是成功的。

源码分析

令牌生成过程

接下来将对上述“令牌持久化”功能进行源码分析,主要涉及到两个过程:一个是remember-me令牌生成过程;另一个是remember-me令牌解析过程。可以发现这个和之前“自动登录”功能的流程基本上是一致的,只是实现类由TokenBasedRememberMeServices变为PersistentTokenBasedRememberMeServices。尽管PersistentTokenBasedRememberMeServices类的源码在前面已经贴出了,但是既然是源码分析,那么就有必要再次对该源码进行阅读。这里贴出几个需要分析的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
protected void onLoginSuccess(HttpServletRequest request, HttpServletResponse response, Authentication successfulAuthentication) {
String username = successfulAuthentication.getName();
this.logger.debug(LogMessage.format("Creating new persistent login for user %s", username));
PersistentRememberMeToken persistentToken = new PersistentRememberMeToken(username, this.generateSeriesData(), this.generateTokenData(), new Date());

try {
this.tokenRepository.createNewToken(persistentToken);
this.addCookie(persistentToken, request, response);
} catch (Exception var7) {
this.logger.error("Failed to save persistent token ", var7);
}

}
protected String generateSeriesData() {
byte[] newSeries = new byte[this.seriesLength];
this.random.nextBytes(newSeries);
return new String(Base64.getEncoder().encode(newSeries));
}

protected String generateTokenData() {
byte[] newToken = new byte[this.tokenLength];
this.random.nextBytes(newToken);
return new String(Base64.getEncoder().encode(newToken));
}

private void addCookie(PersistentRememberMeToken token, HttpServletRequest request, HttpServletResponse response) {
this.setCookie(new String[]{token.getSeries(), token.getTokenValue()}, this.getTokenValiditySeconds(), request, response);
}

简单分析一下上述方法:
(1)由于令牌持久化的前提是用户已经登录成功,因此需要从登录的认证信息中获取用户名,也就是username。
(2)构造一个PersistentRememberMeToken对象,分别调用generateSeriesData()generateTokenData()方法来获取series和token信息。查看一下generateSeriesData()方法的源码,可以发现它首先定义一个byte[]类型的数组,之后调用random.nextBytes()方法来生成随机数,请注意这里的random其实是SecureRandom对象,不同于以前使用的Math.random或者java.util.Random伪随机数,SecureRandom采用的是类似于密码学的随机数生成规则,因此输出的结果难以预测,安全性较高。之后在使用Base64对生成的随机数进行编码,最后进行返回。
(3)回到onLoginSuccess方法中,继续往下看,之后调用tokenRepository.createNewToken(persistentToken)方法将PersistentRememberMeToken对象存入数据库中。其实这里的tokenRepository对象就是前面创建的JdbcTokenRepositoryImpl对象。
(4)调用addCookie()方法将之前创建的PersistentRememberMeToken对象添加到cookie中,可以查看这个addCookie()方法的源码,如下所示:

1
2
3
private void addCookie(PersistentRememberMeToken token, HttpServletRequest request, HttpServletResponse response) {
this.setCookie(new String[]{token.getSeries(), token.getTokenValue()}, this.getTokenValiditySeconds(), request, response);
}

从上述源码就能知道这个Cookie是一个字符串类型的数组,且数组中的元素分别为series、tokenValue、tokenValiditySeconds等信息。

令牌解析过程

说完了令牌的生成过程,接下来开始学习令牌的解析过程,也就是如何从持久化的令牌中获取用户信息。此时就需要阅读PersistentTokenBasedRememberMeServices类中processAutoLoginCookie()方法的源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
protected UserDetails processAutoLoginCookie(String[] cookieTokens, HttpServletRequest request, HttpServletResponse response) {
if (cookieTokens.length != 2) {
throw new InvalidCookieException("Cookie token did not contain 2 tokens, but contained '" + Arrays.asList(cookieTokens) + "'");
} else {
String presentedSeries = cookieTokens[0];
String presentedToken = cookieTokens[1];
PersistentRememberMeToken token = this.tokenRepository.getTokenForSeries(presentedSeries);
if (token == null) {
throw new RememberMeAuthenticationException("No persistent token found for series id: " + presentedSeries);
} else if (!presentedToken.equals(token.getTokenValue())) {
this.tokenRepository.removeUserTokens(token.getUsername());
throw new CookieTheftException(this.messages.getMessage("PersistentTokenBasedRememberMeServices.cookieStolen", "Invalid remember-me token (Series/token) mismatch. Implies previous cookie theft attack."));
} else if (token.getDate().getTime() + (long)this.getTokenValiditySeconds() * 1000L < System.currentTimeMillis()) {
throw new RememberMeAuthenticationException("Remember-me login has expired");
} else {
this.logger.debug(LogMessage.format("Refreshing persistent login token for user '%s', series '%s'", token.getUsername(), token.getSeries()));
PersistentRememberMeToken newToken = new PersistentRememberMeToken(token.getUsername(), token.getSeries(), this.generateTokenData(), new Date());

try {
this.tokenRepository.updateToken(newToken.getSeries(), newToken.getTokenValue(), newToken.getDate());
this.addCookie(newToken, request, response);
} catch (Exception var9) {
this.logger.error("Failed to update token: ", var9);
throw new RememberMeAuthenticationException("Autologin failed due to data access problem");
}

return this.getUserDetailsService().loadUserByUsername(token.getUsername());
}
}
}

分析一下上述方法的执行逻辑:
(1)前面我们通过调用addCookie()方法将之前创建的PersistentRememberMeToken对象添加到cookie中,因此首先判断这个从前端传来的cookie的长度,如果长度不为2,则说明至少缺失series或者tokenValue中的任何一个,因此必须抛出异常,无法进行后续的验证。
(2)从cookieTokens中取出series和tokenValue信息,请注意这里就需要使用到前面的元素序号了。
(3)根据(2)中获取的series信息来调用tokenRepository.getTokenForSeries()方法,进而获取一个PersistentRememberMeToken对象。
(4)判断(3)中得到的PersistentRememberMeToken对象是否为空,如果为空则抛出异常,否则判断PersistentRememberMeToken对象中的tokenValue与之前从前端中传来tokenValue是否相同,如果不相同,则说明账号可能被盗用了(因为只要有新的会话,token就会更新),因此就需要根据用户名来移除相应的token,此时用户就只能通过用户名+密码的方式来重新登录,进而获取新的自动登录权限。
(5)之后进行token是否过期判断,逻辑是通过token生成时间+过期期限的值是否大于当前时间,如果大于则说明此时token还能继续使用,否则说明token已经过期,无法使用。
(6)以上验证通过后,接下来构造一个PersistentRememberMeToken对象,并调用tokenRepository.updateToken()方法来更新数据库中的token,这样就能发现当有一个新的会话诞生时,就会生成一个与之对应的token。
(7)调用addCookie()方法将新生成的token放入其中。
(8)最后再通过用户名来查询用户信息,之后再进行登录操作。

通过以上的分析,相信大家对“令牌持久化”有了一个较为清晰的认识。通过“令牌持久化”,相比于之前的登录安全性,它提升了不止一个等级,但是“令牌持久化”依旧存在用户身份被盗用的问题,这个问题其实是非常难解决的,只能说是最大限度降低被盗发生的可能性。

二次校验

除了前面学习的“令牌持久化”方式,这里还提供另一种方式—二次校验。

我们知道此处引入自动登录的初心是为了提升用户体验,让用户在第一次访问某个页面时,通过输入用户名和密码完成登录后,此后在一定的时间期限内,再次访问该页面则无需登录。但是自动登录又引入了安全风险,这是我们不想看到的。我们希望如果用户使用了自动登录功能,那么只允许它做一些常规的不敏感操作,比如浏览普通页面,查看数据等,但是不允许用户对页面和数据进行修改、删除等操作。且如果用户非要对页面和数据进行修改、删除等操作时,我们可以让页面跳转到登录页面,让用户重新输入密码来验证身份,身份验证通过后再允许它执行一些敏感操作,这就是二次校验的逻辑。

“二次校验”比较经典的应用例子就是GitHub仓库,当开发者需要删除某个仓库时,页面就会跳转到输入用户密码的界面,之后用户输入密码并验证通过后才能删除该仓库。当然偶尔也是输入仓库名称来防止用户误删除仓库,这些都是比较常见的应用场景。

为了后续项目的演示效果,这里需要提供三个接口。在controller包内新建一个HelloController类,之后在该类中新增如下方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@RestController
public class HelloController {
@GetMapping("/hello")
public String hello(){
return "Hello,World!";
}

@GetMapping("/book")
public String book(){
return "Hello,Book!";
}

@GetMapping("/movie")
public String movie(){
return "Hello,Movie!";
}
}

对三个接口的描述如下:
(1)/hello接口,它是只要用户认证后就能访问(即用户成功登陆),注意无论是通过用户名+密码认证还是自动认证,只要通过认证,那么就能访问。
(2)/book接口,它必须是用户通过用户名+密码认证后才能访问,也就是说用户通过自动登录方式认证的,它是无法访问到该接口的。
(3)/movie接口,它必须是用户通过自动认证后才能访问,也就是说用户通过用户名+密码登录方式认证的,它是无法访问到该接口的。

在完成了上述三个接口的定义后,接下来就进行配置,让上述三个接口按照既定逻辑生效。修改MySecurityConfig类中configure(HttpSecurity http)方法的代码为如下所示信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/movie").rememberMe()
.antMatchers("/book").fullyAuthenticated()
.anyRequest().authenticated()
.and()
.formLogin()
.and()
.rememberMe()
.key("envy")
.tokenRepository(jdbcTokenRepository())
.and()
.csrf().disable();
}

简单介绍一下上述添加的配置:
(1).antMatchers("/movie").rememberMe()配置表示/movie接口是rememberMe才能访问,即“自动登录”。查看一下这个rememberMe方法的源码:

1
2
3
public ExpressionUrlAuthorizationConfigurer<H>.ExpressionInterceptUrlRegistry rememberMe() {
return this.access("rememberMe");
}

可以看到它其实是调用了access方法来表示只有”rememberMe”的方式才能认证。
(2).antMatchers("/book").fullyAuthenticated()配置表示/book接口是fullyAuthenticated才能访问,即“全登录”,它不包括自动登录的方式。查看一下这个fullyAuthenticated方法的源码:

1
2
3
public ExpressionUrlAuthorizationConfigurer<H>.ExpressionInterceptUrlRegistry fullyAuthenticated() {
return this.access("fullyAuthenticated");
}

其实ExpressionUrlAuthorizationConfigurer这个类中名为AuthorizedUrl的内部类中提供了很多认证方式,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public ExpressionUrlAuthorizationConfigurer<H>.ExpressionInterceptUrlRegistry permitAll() {
return this.access("permitAll");
}

public ExpressionUrlAuthorizationConfigurer<H>.ExpressionInterceptUrlRegistry anonymous() {
return this.access("anonymous");
}

public ExpressionUrlAuthorizationConfigurer<H>.ExpressionInterceptUrlRegistry rememberMe() {
return this.access("rememberMe");
}

public ExpressionUrlAuthorizationConfigurer<H>.ExpressionInterceptUrlRegistry denyAll() {
return this.access("denyAll");
}

public ExpressionUrlAuthorizationConfigurer<H>.ExpressionInterceptUrlRegistry authenticated() {
return this.access("authenticated");
}

public ExpressionUrlAuthorizationConfigurer<H>.ExpressionInterceptUrlRegistry fullyAuthenticated() {
return this.access("fullyAuthenticated");
}

上面这些都是默认提供的,分别表示所有请求都允许访问(需登录)、匿名访问(无需登录)、记住我登录、禁止访问所有、需要认证(需登录)和全认证(不包括自动登录)。

(3).anyRequest().authenticated()配置表示其余所有的接口都是需要认证后才能访问,即登录后才能访问。

之后开发者注释掉之前使用“令牌持久化”的相关代码,重启项目,之后访问/hello接口,页面就会跳转到登录页面,用户输入密码并勾选“记住我”,点击登录,之后页面会显示/hello接口的内容,同时可以发现也能访问/book接口,但是用户无法访问/movie接口。