写在前面 前一篇学习了如何实现自动登录,但是随之而来的是以牺牲系统安全为代价,这一点在很多场景下都是不可取的,因此就必须对自动登录的核心—令牌进行一些安全提升,或者采用二次验证等方式来提升系统的安全性。
令牌持久化 概念 前面提到过一个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
接口。