Coding & Life

求知若饥,虚心若愚

0%

1. 前言

Json Web TokenJWT) 近几年是前后端分离常用的 Token 技术,是目前最流行的跨域身份验证解决方案。今天我们来手写一个通用的 JWT 服务。DEMO 获取方式在文末,实现在 jwt 相关包下

2. spring-security-jwt

spring-security_jwtSpring Security Crypto提供的JWT工具包

1
2
3
4
5
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-jwt</artifactId>
<version>${spring-security-jwt.version}</version>
</dependency>

核心类只有一个:org.springframework.security.jwt.JwtHelper。它提供了两个非常有用的静态方法。

3. JWT编码

JwtHelper 提供的第一个静态方法就是 encode(CharSequence content, Signer signer) 这个是用来生成jwt的方法 需要指定 payloadsigner 签名算法。payload 存放了一些可用的不敏感信息:

  • iss jwt签发者
  • sub jwt所面向的用户
  • aud 接收jwt的一方
  • iat jwt的签发时间
  • exp jwt的过期时间,这个过期时间必须要大于签发时间iat
  • jti jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击

除了以上提供的基本信息外,我们可以定义一些我们需要传递的信息,比如目标用户的权限集 等等。切记不要传递密码等敏感信息 ,因为 JWT 的前两段都是用了 BASE64 编码,几乎算是明文了。

3.1 构建JWT中的payload

我们先来构建payload:

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
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
/**
* 构建 jwt payload
*
* @author Felordcn
* @since 11:27 2019/10/25
**/
public class JwtPayloadBuilder {

private Map<String, String> payload = new HashMap<>();
/**
* 附加的属性
*/
private Map<String, String> additional;
/**
* jwt签发者
**/
private String iss;
/**
* jwt所面向的用户
**/
private String sub;
/**
* 接收jwt的一方
**/
private String aud;
/**
* jwt的过期时间,这个过期时间必须要大于签发时间
**/
private LocalDateTime exp;
/**
* jwt的签发时间
**/
private LocalDateTime iat = LocalDateTime.now();
/**
* 权限集
*/
private Set<String> roles = new HashSet<>();
/**
* jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击
**/
private String jti = IdUtil.simpleUUID();

public JwtPayloadBuilder iss(String iss) {
this.iss = iss;
return this;
}


public JwtPayloadBuilder sub(String sub) {
this.sub = sub;
return this;
}

public JwtPayloadBuilder aud(String aud) {
this.aud = aud;
return this;
}


public JwtPayloadBuilder roles(Set<String> roles) {
this.roles = roles;
return this;
}

public JwtPayloadBuilder expDays(int days) {
Assert.isTrue(days > 0, "jwt expireDate must after now");
this.exp = this.iat.plusDays(days);
return this;
}

public JwtPayloadBuilder additional(Map<String, String> additional) {
this.additional = additional;
return this;
}

public String builder() {
payload.put("iss", this.iss);
payload.put("sub", this.sub);
payload.put("aud", this.aud);
payload.put("exp", this.exp.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
payload.put("iat", this.iat.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
payload.put("jti", this.jti);

if (!CollectionUtils.isEmpty(additional)) {
payload.putAll(additional);
}
payload.put("roles", JSONUtil.toJsonStr(this.roles));
return JSONUtil.toJsonStr(JSONUtil.parse(payload));

}

}

通过建造类 JwtPayloadBuilder 我们可以很方便来构建 JWT 所需要的 payload json 字符串传递给 encode(CharSequence content, Signer signer) 中的 content

3.2 生成 RSA 密钥并进行签名

为了生成 JWT Token 我们还需要使用 RSA 算法来进行签名。 这里我们使用 JDK 提供的证书管理工具 Keytool 来生成 RSA 证书 ,格式为 jks 格式。

生成证书命令参考:

1
keytool -genkey -alias felordcn -keypass felordcn -keyalg RSA -storetype PKCS12 -keysize 1024 -validity 365 -keystore d:/keystores/felordcn.jks -storepass 123456  -dname "CN=(Felord), OU=(felordcn), O=(felordcn), L=(zz), ST=(hn), C=(cn)"

其中 -alias felordcn -storepass 123456 我们要作为配置使用要记下来。我们要使用下面定义的这个类来读取证书

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
package cn.felord.spring.security.jwt;

import org.springframework.core.io.ClassPathResource;

import java.security.KeyFactory;
import java.security.KeyPair;
import java.security.KeyStore;
import java.security.PublicKey;
import java.security.interfaces.RSAPrivateCrtKey;
import java.security.spec.RSAPublicKeySpec;

/**
* KeyPairFactory
*
* @author Felordcn
* @since 13:41 2019/10/25
**/
class KeyPairFactory {

private KeyStore store;

private final Object lock = new Object();

/**
* 获取公私钥.
*
* @param keyPath jks 文件在 resources 下的classpath
* @param keyAlias keytool 生成的 -alias 值 felordcn
* @param keyPass keytool 生成的 -storepass 值 123456
* @return the key pair 公私钥对
*/
KeyPair create(String keyPath, String keyAlias, String keyPass) {
ClassPathResource resource = new ClassPathResource(keyPath);
char[] pem = keyPass.toCharArray();
try {
synchronized (lock) {
if (store == null) {
synchronized (lock) {
store = KeyStore.getInstance("jks");
store.load(resource.getInputStream(), pem);
}
}
}
RSAPrivateCrtKey key = (RSAPrivateCrtKey) store.getKey(keyAlias, pem);
RSAPublicKeySpec spec = new RSAPublicKeySpec(key.getModulus(), key.getPublicExponent());
PublicKey publicKey = KeyFactory.getInstance("RSA").generatePublic(spec);
return new KeyPair(publicKey, key);
} catch (Exception e) {
throw new IllegalStateException("Cannot load keys from store: " + resource, e);
}

}
}

获取了 KeyPair 就能获取公私钥 生成 Jwt 的两个要素就完成了。我们可以和之前定义的 JwtPayloadBuilder 一起封装出生成 Jwt Token 的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
private String jwtToken(String aud, int exp, Set<String> roles, Map<String, String> additional) {
String payload = jwtPayloadBuilder
.iss(jwtProperties.getIss())
.sub(jwtProperties.getSub())
.aud(aud)
.additional(additional)
.roles(roles)
.expDays(exp)
.builder();
RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate();

RsaSigner signer = new RsaSigner(privateKey);
return JwtHelper.encode(payload, signer).getEncoded();
}

通常情况下 Jwt Token 都是成对出现的,一个为平常请求携带的 accessToken, 另一个只作为刷新 accessToken 之用的 refreshToken 。而且 refreshToken 的过期时间要相对长一些。当 accessToken 失效而refreshToken 有效时,我们可以通过 refreshToken 来获取新的 Jwt Token对 ;当两个都失效就用户就必须重新登录了。

生成 Jwt Token对 的方法如下:

1
2
3
4
5
6
7
8
9
10
11
public JwtTokenPair jwtTokenPair(String aud, Set<String> roles, Map<String, String> additional) {
String accessToken = jwtToken(aud, jwtProperties.getAccessExpDays(), roles, additional);
String refreshToken = jwtToken(aud, jwtProperties.getRefreshExpDays(), roles, additional);

JwtTokenPair jwtTokenPair = new JwtTokenPair();
jwtTokenPair.setAccessToken(accessToken);
jwtTokenPair.setRefreshToken(refreshToken);
// 放入缓存
jwtTokenStorage.put(jwtTokenPair, aud);
return jwtTokenPair;
}

通常 Jwt Token对 会在返回给前台的同时放入缓存中。过期策略你可以选择分开处理,也可以选择以refreshToken的过期时间为准。

4. JWT解码以及验证

JwtHelper 提供的第二个静态方法是Jwt decodeAndVerify(String token, SignatureVerifier verifier) 用来 验证和解码 Jwt Token 。我们获取到请求中的token后会解析出用户的一些信息。通过这些信息去缓存中对应的token ,然后比对并验证是否有效(包括是否过期)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
* 解码 并校验签名 过期不予解析
*
* @param jwtToken the jwt token
* @return the jwt claims
*/
public JSONObject decodeAndVerify(String jwtToken) {
Assert.hasText(jwtToken, "jwt token must not be bank");
RSAPublicKey rsaPublicKey = (RSAPublicKey) this.keyPair.getPublic();
SignatureVerifier rsaVerifier = new RsaVerifier(rsaPublicKey);
Jwt jwt = JwtHelper.decodeAndVerify(jwtToken, rsaVerifier);
String claims = jwt.getClaims();
JSONObject jsonObject = JSONUtil.parseObj(claims);
String exp = jsonObject.getStr(JWT_EXP_KEY);
// 是否过期
if (isExpired(exp)) {
throw new IllegalStateException("jwt token is expired");
}
return jsonObject;
}

上面我们将有效的 Jwt Token 中的 payload 解析为 JSON对象 ,方便后续的操作。

5. 配置

我们将 JWT 的可配置项抽出来放入 JwtProperties 如下:

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
/**
* Jwt 在 springboot application.yml 中的配置文件
*
* @author Felordcn
* @since 15 :06 2019/10/25
*/
@Data
@ConfigurationProperties(prefix=JWT_PREFIX)
public class JwtProperties {
static final String JWT_PREFIX= "jwt.config";
/**
* 是否可用
*/
private boolean enabled;
/**
* jks 路径
*/
private String keyLocation;
/**
* key alias
*/
private String keyAlias;
/**
* key store pass
*/
private String keyPass;
/**
* jwt签发者
**/
private String iss;
/**
* jwt所面向的用户
**/
private String sub;
/**
* access jwt token 有效天数
*/
private int accessExpDays;
/**
* refresh jwt token 有效天数
*/
private int refreshExpDays;
}

然后我们就可以配置JWTjavaConfig如下:

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
/**
* JwtConfiguration
*
* @author Felordcn
* @since 16 :54 2019/10/25
*/
@EnableConfigurationProperties(JwtProperties.class)
@ConditionalOnProperty(prefix = "jwt.config",name = "enabled")
@Configuration
public class JwtConfiguration {


/**
* Jwt token storage .
*
* @return the jwt token storage
*/
@Bean
public JwtTokenStorage jwtTokenStorage() {
return new JwtTokenCacheStorage();
}


/**
* Jwt token generator.
*
* @param jwtTokenStorage the jwt token storage
* @param jwtProperties the jwt properties
* @return the jwt token generator
*/
@Bean
public JwtTokenGenerator jwtTokenGenerator(JwtTokenStorage jwtTokenStorage, JwtProperties jwtProperties) {
return new JwtTokenGenerator(jwtTokenStorage, jwtProperties);
}

}

然后你就可以通过 JwtTokenGenerator 编码/解码验证 Jwt Token 对 ,通过 JwtTokenStorage 来处理 Jwt Token 缓存。缓存这里我用了Spring Cache Ehcache 来实现,你也可以切换到 Redis 。相关单元测试参见 DEMO

6. 总结

今天我们利用 spring-security-jwt 手写了一套 JWT 逻辑。无论对你后续结合 Spring Security 还是 Shiro 都十分有借鉴意义。下一篇我们会讲解 JWT 结合Spring Security

本节代码分支在day05

转载自@felord.cn

1. 前言

上一篇对 Spring Security 所有内置的 Filter 进行了介绍。今天我们来实战如何安全退出应用程序。

2. 我们使用 Spring Security 登录后都做了什么

这个问题我们必须搞清楚!一般登录后,服务端会给用户发一个凭证。常见有以下的两种:

  • 基于 Session 客户端会存 cookie 来保存一个 sessionId ,服务端存一个 Session
  • 基于 token 客户端存一个 token 串,服务端会在缓存中存一个用来校验此 token 的信息。

3. 退出登录需要我们做什么

  1. 当前的用户登录状态失效。这就需要我们清除服务端的用户状态。
  2. 退出登录接口并不是 permitAll, 只有携带对应用户的凭证才退出。
  3. 将退出结果返回给请求方。
  4. 退出登录后用户可以通过重新登录来认证该用户。

4. Spring Security 中的退出登录

接下来我们来分析并实战 如何定制退出登录逻辑。首先我们要了解 LogoutFilter 。

4.1 LogoutFilter

通过前文我们知道退出登录逻辑是由过滤器 LogoutFilter 来执行的。 它持有三个接口类型的属性:

  1. RequestMatcher logoutRequestMatcher 这个用来拦截退出请求的 URL
  2. LogoutHandler handler 用来处理退出的具体逻辑
  3. LogoutSuccessHandler logoutSuccessHandler 退出成功后执行的逻辑

我们通过对以上三个接口的实现就能实现我们自定义的退出逻辑。

4.2 LogoutConfigurer

我们一般不会直接操作 LogoutFilter ,而是通过 LogoutConfigurer 来配置 LogoutFilter。 你可以通过 HttpSecurity.logout() 方法来初始化一个 LogoutConfigurer 。 接下来我们来实战操作一下。

4.2.1 实现自定义退出登录请求URL

LogoutConfigurer 提供了 logoutRequestMatcher(RequestMatcher logoutRequestMatcher)logoutUrl(Sring logoutUrl) 两种方式来定义退出登录请求的 URL 。它们作用是相同的,你选择其中一种方式即可。

4.2.2 处理具体的逻辑

默认情况下 Spring Security 是基于 Session 的。LogoutConfigurer 提供了一些直接配置来满足你的需要。如下:

  • clearAuthentication(boolean clearAuthentication) 是否在退出时清除当前用户的认证信息
  • deleteCookies(String... cookieNamesToClear) 删除指定的 cookies
  • invalidateHttpSession(boolean invalidateHttpSession) 是否移除 HttpSession

如果上面满足不了你的需要就需要你来定制 LogoutHandler 了。

4.2.3 退出成功逻辑

  • logoutSuccessUrl(String logoutSuccessUrl) 退出成功后会被重定向到此 URL你可以写一个Controller 来完成最终返回,但是需要支持 GET 请求和 匿名访问 。 通过 setDefaultTargetUrl 方法注入到 LogoutSuccessHandler
  • defaultLogoutSuccessHandlerFor(LogoutSuccessHandler handler, RequestMatcher preferredMatcher) 用来构造默认的 LogoutSuccessHandler 我们可以通过添加多个来实现从不同 URL 退出执行不同的逻辑。
  • LogoutSuccessHandler logoutSuccessHandler 退出成功后执行的逻辑的抽象根本接口。

4.3 Spring Security 退出登录实战

现在前后端分离比较多,退出后返回json。 而且只有用户在线才能退出登录。否则不能进行退出操作。我们采用实现 LogoutHandlerLogoutSuccessHandler 接口这种编程的方式来配置 。退出请求的 url 依然通过 LogoutConfigurer.logoutUrl(String logoutUrl)来定义。

4.3.1 自定义 LogoutHandler

默认情况下清除认证信息 (invalidateHttpSession),和Session 失效(invalidateHttpSession) 已经由内置的SecurityContextLogoutHandler来完成。我们自定义的LogoutHandler 会在SecurityContextLogoutHandler` 来执行。

1
2
3
4
5
6
7
8
9
@Slf4j
public class CustomLogoutHandler implements LogoutHandler {
@Override
public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) {
User user = (User) authentication.getPrincipal();
String username = user.getUsername();
log.info("username: {} is offline now", username);
}
}

以上是我们实现的 LogoutHandler 。 我们可以从 logout 方法的 authentication 变量中 获取当前用户信息。你可以通过这个来实现你具体想要的业务。比如记录用户下线退出时间、IP 等等。

4.3.2 自定义 LogoutSuccessHandler

如果我们实现了自定义的 LogoutSuccessHandler 就不必要设置 LogoutConfigurer#logoutSuccessUrl(String logoutSuccessUrl) 了。该处理器处理后会响应给前端。你可以转发到其它控制器。重定向到登录页面,也可以自行实现其它 MediaType ,可以是 json 或者页面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Slf4j
public class CustomLogoutSuccessHandler implements LogoutSuccessHandler {
@Override
public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
User user = (User) authentication.getPrincipal();
String username = user.getUsername();
log.info("username: {} is offline now", username);


responseJsonWriter(response, RestBody.ok("退出成功"));
}

private static void responseJsonWriter(HttpServletResponse response, Rest rest) throws IOException {
response.setStatus(HttpServletResponse.SC_OK);
response.setCharacterEncoding("utf-8");
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
ObjectMapper objectMapper = new ObjectMapper();
String resBody = objectMapper.writeValueAsString(rest);
PrintWriter printWriter = response.getWriter();
printWriter.print(resBody);
printWriter.flush();
printWriter.close();
}
}

4.3.3 自定义退出的 Spring Security 配置

为了方便调试我 注释掉了我们 实现的自定义登录,你可以通过 http:localhost:8080/login 来登录,然后通过 http:localhost:8080/logout 测试退出。

1
2
3
4
5
6
7
8
9
10
11
12
13
     @Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.cors()
.and()
.authorizeRequests().anyRequest().authenticated()
.and()
// .addFilterBefore(preLoginFilter, UsernamePasswordAuthenticationFilter.class)
// 登录
.formLogin().loginProcessingUrl(LOGIN_PROCESSING_URL).successForwardUrl("/login/success").failureForwardUrl("/login/failure")
.and().logout().addLogoutHandler(new CustomLogoutHandler()).logoutSuccessHandler(new CustomLogoutSuccessHandler());

}

5. 总结

本篇 我们实现了 在 Spring Security 下的自定义退出逻辑。相对比较简单,你可以根据你的业务需要来实现你的退出逻辑。代码在day04分支

转载自@felord.cn

1. 前言

上一文我们使用 Spring Security 实现了各种登录聚合的场面。其中我们是通过在 UsernamePasswordAuthenticationFilter 之前一个自定义的过滤器实现的。我怎么知道自定义过滤器要加在 UsernamePasswordAuthenticationFilter 之前。我在这个系列开篇说了 Spring Security 权限控制的一个核心关键就是 过滤器链 ,这些过滤器如下图进行过滤传递,甚至比这个更复杂!这只是一个最小单元。

Spring Security 内置了一些过滤器,他们各有各的本事。如果你掌握了这些过滤器,很多实际开发中的需求和问题都很容易解决。今天我们来见识一下这些内置的过滤器。

2. 内置过滤器初始化

Spring Security 初始化核心过滤器时 HttpSecurity 会通过将 Spring Security 内置的一些过滤器以 FilterComparator 提供的规则进行比较按照比较结果进行排序注册。

2.1 排序规则

FilterComparator 维护了一个顺序的注册表 filterToOrder

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
FilterComparator() {
Step order = new Step(INITIAL_ORDER, ORDER_STEP);
put(ChannelProcessingFilter.class, order.next());
put(ConcurrentSessionFilter.class, order.next());
put(WebAsyncManagerIntegrationFilter.class, order.next());
put(SecurityContextPersistenceFilter.class, order.next());
put(HeaderWriterFilter.class, order.next());
put(CorsFilter.class, order.next());
put(CsrfFilter.class, order.next());
put(LogoutFilter.class, order.next());
filterToOrder.put(
"org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestRedirectFilter",
order.next());
filterToOrder.put(
"org.springframework.security.saml2.provider.service.servlet.filter.Saml2WebSsoAuthenticationRequestFilter",
order.next());
put(X509AuthenticationFilter.class, order.next());
put(AbstractPreAuthenticatedProcessingFilter.class, order.next());
filterToOrder.put("org.springframework.security.cas.web.CasAuthenticationFilter",
order.next());
filterToOrder.put(
"org.springframework.security.oauth2.client.web.OAuth2LoginAuthenticationFilter",
order.next());
filterToOrder.put(
"org.springframework.security.saml2.provider.service.servlet.filter.Saml2WebSsoAuthenticationFilter",
order.next());
put(UsernamePasswordAuthenticationFilter.class, order.next());
put(ConcurrentSessionFilter.class, order.next());
filterToOrder.put(
"org.springframework.security.openid.OpenIDAuthenticationFilter", order.next());
put(DefaultLoginPageGeneratingFilter.class, order.next());
put(DefaultLogoutPageGeneratingFilter.class, order.next());
put(ConcurrentSessionFilter.class, order.next());
put(DigestAuthenticationFilter.class, order.next());
filterToOrder.put(
"org.springframework.security.oauth2.server.resource.web.BearerTokenAuthenticationFilter", order.next());
put(BasicAuthenticationFilter.class, order.next());
put(RequestCacheAwareFilter.class, order.next());
put(SecurityContextHolderAwareRequestFilter.class, order.next());
put(JaasApiIntegrationFilter.class, order.next());
put(RememberMeAuthenticationFilter.class, order.next());
put(AnonymousAuthenticationFilter.class, order.next());
filterToOrder.put(
"org.springframework.security.oauth2.client.web.OAuth2AuthorizationCodeGrantFilter",
order.next());
put(SessionManagementFilter.class, order.next());
put(ExceptionTranslationFilter.class, order.next());
put(FilterSecurityInterceptor.class, order.next());
put(SwitchUserFilter.class, order.next());
}

这些就是所有内置的过滤器。他们是通过下面的方法获取自己的序号:

1
2
3
4
5
6
7
8
9
10
private Integer getOrder(Class<?> clazz) {
while (clazz != null) {
Integer result = filterToOrder.get(clazz.getName());
if (result != null) {
return result;
}
clazz = clazz.getSuperclass();
}
return null;
}

通过过滤器的类全限定名从注册表 filterToOrder 中获取自己的序号,如果没有直接获取到序号通过递归获取父类在注册表中的序号作为自己的序号,序号越小优先级越高。上面的过滤器并非全部会被初始化。有的需要额外引入一些功能包,有的看 HttpSecurity 的配置情况。 在上一篇文章中。我们禁用了 CSRF 功能,就意味着 CsrfFilter 不会被注册。

3. 内置过滤器讲解

接下来我们就对这些内置过滤器进行一个系统的认识。我们将按照默认顺序进行讲解。

3.1 ChannelProcessingFilter

ChannelProcessingFilter 通常是用来过滤哪些请求必须用 https 协议, 哪些请求必须用 http 协议, 哪些请求随便用哪个协议都行。它主要有两个属性:

  • ChannelDecisionManager 用来判断请求是否符合既定的协议规则。它维护了一个 ChannelProcessor 列表 这些ChannelProcessor 是具体用来执行 ANY_CHANNEL 策略 (任何通道都可以), REQUIRES_SECURE_CHANNEL 策略 (只能通过https通道), REQUIRES_INSECURE_CHANNEL 策略 (只能通过 http 通道)。

  • FilterInvocationSecurityMetadataSource 用来存储 url 与 对应的ANY_CHANNELREQUIRES_SECURE_CHANNELREQUIRES_INSECURE_CHANNEL 的映射关系。

ChannelProcessingFilter通过 HttpScurity#requiresChannel() 等相关方法引入其配置对象 ChannelSecurityConfigurer 来进行配置。

3.2 ConcurrentSessionFilter

ConcurrentSessionFilter 主要用来判断session是否过期以及更新最新的访问时间。其流程为:

  1. session 检测,如果不存在直接放行去执行下一个过滤器。存在则进行下一步。
  2. 根据sessionidSessionRegistry中获取SessionInformation,从SessionInformation中获取session是否过期;没有过期则更新SessionInformation中的访问日期;
    如果过期,则执行doLogout()方法,这个方法会将session无效,并将 SecurityContext 中的Authentication中的权限置空,同时在SecurityContenxtHoloder中清除SecurityContext然后查看是否有跳转的 expiredUrl,如果有就跳转,没有就输出提示信息。

ConcurrentSessionFilter 通过SessionManagementConfigurer 来进行配置。

3.3 WebAsyncManagerIntegrationFilter

WebAsyncManagerIntegrationFilter用于集成SecurityContext到Spring异步执行机制中的WebAsyncManager。用来处理异步请求的安全上下文。具体逻辑为:

  1. 从请求属性上获取所绑定的WebAsyncManager,如果尚未绑定,先做绑定。
  2. asyncManager中获取 keyCALLABLE_INTERCEPTOR_KEY 的安全上下文多线程处理器 SecurityContextCallableProcessingInterceptor, 如果获取到的为 null
    新建一个 SecurityContextCallableProcessingInterceptor 并绑定 CALLABLE_INTERCEPTOR_KEY 注册到 asyncManager 中。

这里简单说一下 SecurityContextCallableProcessingInterceptor 。它实现了接口 CallableProcessingInterceptor
当它被应用于一次异步执行时,beforeConcurrentHandling() 方法会在调用者线程执行,该方法会相应地从当前线程获取SecurityContext,然后被调用者线程中执行逻辑时,会使用这个 SecurityContext,从而实现安全上下文从调用者线程到被调用者线程的传输。

WebAsyncManagerIntegrationFilter 通过 WebSecurityConfigurerAdapter#getHttp()方法添加到 HttpSecurity 中成为 DefaultSecurityFilterChain 的一个链节。

3.4 SecurityContextPersistenceFilter

SecurityContextPersistenceFilter 主要控制 SecurityContext 的在一次请求中的生命周期 。 请求来临时,创建SecurityContext 安全上下文信息,请求结束时清空 SecurityContextHolder

SecurityContextPersistenceFilter 通过 HttpScurity#securityContext() 及相关方法引入其配置对象 SecurityContextConfigurer 来进行配置。

3.5 HeaderWriterFilter

HeaderWriterFilter 用来给 http 响应添加一些 Header,比如 X-Frame-Options, X-XSS-ProtectionX-Content-Type-Options

你可以通过 HttpScurity#headers() 来定制请求Header

3.6 CorsFilter

跨域相关的过滤器。这是Spring MVC Java配置和XML 命名空间 CORS 配置的替代方法, 仅对依赖于spring-web的应用程序有用(不适用于spring-webmvc)或 要求在javax.servlet.Filter 级别进行CORS检查的安全约束链接。这个是目前官方的一些解读,但是我还是不太清楚实际机制。

你可以通过 HttpSecurity#cors() 来定制。

3.7 CsrfFilter

CsrfFilter 用于防止csrf攻击,前后端使用json交互需要注意的一个问题。

你可以通过 HttpSecurity.csrf() 来开启或者关闭它。在你使用 jwttoken 技术时,是不需要这个的。

3.8 LogoutFilter

LogoutFilter 很明显这是处理注销的过滤器。

你可以通过 HttpSecurity.logout() 来定制注销逻辑,非常有用。

3.9 OAuth2AuthorizationRequestRedirectFilter

和上面的有所不同,这个需要依赖 spring-scurity-oauth2 相关的模块。该过滤器是处理 OAuth2 请求首选重定向相关逻辑的。以后会我会带你们认识它,请多多关注公众号:Felordcn

3.10 Saml2WebSsoAuthenticationRequestFilter

这个需要用到 Spring Security SAML 模块,这是一个基于 SMALSSO 单点登录请求认证过滤器。

关于SAM

SAML 即安全断言标记语言,英文全称是 Security Assertion Markup Language。它是一个基于 XML 的标准,用于在不同的安全域(security domain)之间交换认证和授权数据。在 SAML 标准定义了身份提供者 (identity provider) 和服务提供者 (service provider),这两者构成了前面所说的不同的安全域。 SAMLOASIS 组织安全服务技术委员会(Security Services Technical Committee) 的产品。

SAMLSecurity Assertion Markup Language)是一个 XML 框架,也就是一组协议,可以用来传输安全声明。比如,两台远程机器之间要通讯,为了保证安全,我们可以采用加密等措施,也可以采用 SAML 来传输,传输的数据以 XML 形式,符合 SAML 规范,这样我们就可以不要求两台机器采用什么样的系统,只要求能理解 SAML 规范即可,显然比传统的方式更好。SAML 规范是一组 Schema 定义。

可以这么说,在 Web Service 领域,schema 就是规范,在 Java 领域,API 就是规范

3.11 X509AuthenticationFilter

X509 认证过滤器。你可以通过 HttpSecurity#X509() 来启用和配置相关功能。

3.12 AbstractPreAuthenticatedProcessingFilter

AbstractPreAuthenticatedProcessingFilter 处理经过预先认证的身份验证请求的过滤器的基类,其中认证主体已经由外部系统进行了身份验证。 目的只是从传入请求中提取主体上的必要信息,而不是对它们进行身份验证。

你可以继承该类进行具体实现并通过 HttpSecurity#addFilter 方法来添加个性化的AbstractPreAuthenticatedProcessingFilter

3.13 CasAuthenticationFilter

CAS 单点登录认证过滤器 。依赖 Spring Security CAS 模块

3.14 OAuth2LoginAuthenticationFilter

这个需要依赖 spring-scurity-oauth2 相关的模块。OAuth2 登录认证过滤器。处理通过 OAuth2 进行认证登录的逻辑。

3.15 Saml2WebSsoAuthenticationFilter

这个需要用到 Spring Security SAML 模块,这是一个基于 SMALSSO 单点登录认证过滤器。关于SAML

3.16 UsernamePasswordAuthenticationFilter

这个看过我相关文章的应该不陌生了。处理用户以及密码认证的核心过滤器。认证请求提交的usernamepassword,被封装成token进行一系列的认证,便是主要通过这个过滤器完成的,在表单认证的方法中,这是最最关键的过滤器。

你可以通过 HttpSecurity.formLogin() 及相关方法引入其配置对象 FormLoginConfigurer 来进行配置。 我们在Spring Security6 - 玩转自定义登录 已经对其进行过个性化的配置和魔改。

3.17 ConcurrentSessionFilter

参见 3.2 ConcurrentSessionFilter。 该过滤器可能会被多次执行。

3.18 OpenIDAuthenticationFilter

基于OpenID 认证协议的认证过滤器。 你需要在依赖中依赖额外的相关模块才能启用它。

3.19 DefaultLoginPageGeneratingFilter

生成默认的登录页。默认 /login

3.20 DefaultLogoutPageGeneratingFilter

生成默认的退出页。 默认 /logout

3.21 ConcurrentSessionFilter

参见 3.2 ConcurrentSessionFilter。 该过滤器可能会被多次执行。

3.23 DigestAuthenticationFilter

Digest身份验证是 Web 应用程序中流行的可选的身份验证机制 。DigestAuthenticationFilter 能够处理 HTTP 头中显示的摘要式身份验证凭据。你可以通过 HttpSecurity.addFilter() 来启用和配置相关功能。

3.24 BasicAuthenticationFilter

Digest身份验证一样都是Web 应用程序中流行的可选的身份验证机制 。 BasicAuthenticationFilter 负责处理 HTTP 头中显示的基本身份验证凭据。这个 Spring SecuritySpring Boot 自动配置默认是启用的 。

BasicAuthenticationFilter 通过 HttpSecurity.httpBasic() 及相关方法引入其配置对象 HttpBasicConfigurer 来进行配置。

3.25 RequestCacheAwareFilter

用于用户认证成功后,重新恢复因为登录被打断的请求。当匿名访问一个需要授权的资源时。会跳转到认证处理逻辑,此时请求被缓存。在认证逻辑处理完毕后,从缓存中获取最开始的资源请求进行再次请求。

RequestCacheAwareFilter 通过 HttpScurity.requestCache() 及相关方法引入其配置对象 RequestCacheConfigurer 来进行配置。

3.26 SecurityContextHolderAwareRequestFilter

用来 实现j2eeServlet Api 一些接口方法, 比如 getRemoteUser 方法、isUserInRole 方法,在使用 Spring Security 时其实就是通过这个过滤器来实现的。

SecurityContextHolderAwareRequestFilter 通过 HttpSecurity.servletApi() 及相关方法引入其配置对象 ServletApiConfigurer 来进行配置。

3.27 JaasApiIntegrationFilter

适用于JAASJava 认证授权服务)。 如果 SecurityContextHolder 中拥有的 Authentication 是一个 JaasAuthenticationToken,那么该 JaasApiIntegrationFilter 将使用包含在 JaasAuthenticationToken 中的 Subject 继续执行 FilterChain

3.28 RememberMeAuthenticationFilter

处理 记住我 功能的过滤器。

RememberMeAuthenticationFilter 通过 HttpSecurity.rememberMe() 及相关方法引入其配置对象 RememberMeConfigurer 来进行配置。

3.29 AnonymousAuthenticationFilter

匿名认证过滤器。对于 Spring Security 来说,所有对资源的访问都是有 Authentication 的。对于无需登录(UsernamePasswordAuthenticationFilter )直接可以访问的资源,会授予其匿名用户身份。

AnonymousAuthenticationFilter 通过 HttpSecurity.anonymous() 及相关方法引入其配置对象 AnonymousConfigurer 来进行配置。

3.30 SessionManagementFilter

Session 管理器过滤器,内部维护了一个 SessionAuthenticationStrategy 用于管理 Session

SessionManagementFilter 通过 HttpScurity.sessionManagement() 及相关方法引入其配置对象 SessionManagementConfigurer 来进行配置。

3.31 ExceptionTranslationFilter

主要来传输异常事件,还记得之前我们见过的 DefaultAuthenticationEventPublisher 吗?

3.32 FilterSecurityInterceptor

这个过滤器决定了访问特定路径应该具备的权限,访问的用户的角色,权限是什么?访问的路径需要什么样的角色和权限?这些判断和处理都是由该类进行的。如果你要实现动态权限控制就必须研究该类

3.33 SwitchUserFilter

SwitchUserFilter 是用来做账户切换的。默认的切换账号的url/login/impersonate,默认注销切换账号的url/logout/impersonate,默认的账号参数为username

你可以通过此类实现自定义的账户切换。

4. 总结

所有内置的 31个过滤器作用都讲解完了,有一些默认已经启用。有一些需要引入特定的包并且对 HttpSecurity 进行配置才会生效 。而且它们的顺序是既定的。 只有你了解这些过滤器你才能基于业务深度定制 Spring Security

转载自@felord.cn

1. 前言

安全访问的第一步就是认证(Authentication),认证的第一步就是登录。今天我们要通过对 Spring Security 的自定义,来设计一个可扩展,可伸缩的 form 登录功能。

2. form 登录的流程

下面是form登录的基本流程:

只要是 form 登录基本都能转化为上面的流程。接下来我们看看 Spring Security 是如何处理的。

3. Spring Security 中的登录

上一篇中已经讲到了我们通常的自定义访问控制主要是通过 HttpSecurity 来构建的。默认它提供了三种登录方式:

  • formLogin() 普通表单登录
  • oauth2Login() 基于 OAuth2.0 认证/授权协议
  • openidLogin() 基于 OpenID 身份认证规范

以上三种方式统统是 AbstractAuthenticationFilterConfigurer 实现的。

4. HttpSecurity 中的 form 表单登录

启用表单登录通过两种方式一种是通过 HttpSecurityapply(C configurer) 方法自己构造一个 AbstractAuthenticationFilterConfigurer 的实现,这种是比较高级的玩法。 另一种是我们常见的使用 HttpSecurityformLogin() 方法来自定义 FormLoginConfigurer 。我们先搞一下比较常规的第二种。

4.1 FormLoginConfigurer

该类是 form 表单登录的配置类。它提供了一些我们常用的配置方法:

  • loginPage(String loginPage) : 登录页面而并不是接口,对于前后分离模式需要我们进行改造 默认为 /login
  • loginProcessingUrl(String loginProcessingUrl)实际表单向后台提交用户信息的 Action,再由过滤器UsernamePasswordAuthenticationFilter拦截处理,该 Action 其实不会处理任何逻辑。
  • usernameParameter(String usernameParameter) 用来自定义用户参数名,默认 username
  • passwordParameter(String passwordParameter)用来自定义用户密码名,默认password
  • failureUrl(String authenticationFailureUrl)登录失败后会重定向到此路径, 一般前后分离不会使用它。
  • failureForwardUrl(String forwardUrl) 登录失败会转发到此, 一般前后分离用到它。 可定义一个 Controller (控制器)来处理返回值,但是要注意 RequestMethod
  • defaultSuccessUrl(String defaultSuccessUrl, boolean alwaysUse) 默认登陆成功后跳转到此,如果 alwaysUsetrue 只要进行认证流程而且成功,会一直跳转到此。一般推荐默认值 false
  • successForwardUrl(String forwardUrl) 效果等同于上面 defaultSuccessUrlalwaysUsetrue 但是要注意 RequestMethod
  • successHandler(AuthenticationSuccessHandler successHandler)自定义认证成功处理器,可替代上面所有的 success 方式
  • failureHandler(AuthenticationFailureHandler authenticationFailureHandler) 自定义失败处理器,可替代上面所有的 failure 方式
  • permitAll(boolean permitAll) form 表单登录是否放开

知道了这些我们就能来搞个定制化的登录了。

5. Spring Security 聚合登录 实战

接下来是我们最激动人心的实战登录操作。 有疑问的可认真阅读以往的预热文章

5.1 简单需求

我们的接口访问都要通过认证,登陆错误后返回错误信息(json),成功后前台可以获取到对应数据库用户信息(json)(实战中记得脱敏)。

我们定义处理成功失败的控制器:

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
@RestController
@RequestMapping("/login")
public class LoginController {
@Resource
private SysUserService sysUserService;

/**
* 登录失败返回 401 以及提示信息.
*
* @return the rest
*/
@PostMapping("/failure")
public Rest loginFailure() {

return RestBody.failure(HttpStatus.UNAUTHORIZED.value(), "登录失败了,老哥");
}

/**
* 登录成功后拿到个人信息.
*
* @return the rest
*/
@PostMapping("/success")
public Rest loginSuccess() {
// 登录成功后用户的认证信息 UserDetails会存在 安全上下文寄存器 SecurityContextHolder 中
User principal = (User) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
String username = principal.getUsername();
SysUser sysUser = sysUserService.queryByUsername(username);
// 脱敏
sysUser.setEncodePassword("[PROTECT]");
return RestBody.okData(sysUser,"登录成功");
}
}

然后,我们自定义配置重写void configure(HttpSecurity http)方法进行如下配置(这里需要禁用crsf)

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
@Configuration
@ConditionalOnClass(WebSecurityConfigurerAdapter.class)
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET)
public class CustomSpringBootWebSecurityConfiguration {

@Configuration
@Order(SecurityProperties.BASIC_AUTH_ORDER)
static class DefaultConfigurerAdapter extends WebSecurityConfigurerAdapter {
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
super.configure(auth);
}

@Override
public void configure(WebSecurity web) throws Exception {
super.configure(web);
}

@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.cors()
.and()
.authorizeRequests().anyRequest().authenticated()
.and()
.formLogin()
.loginProcessingUrl("/process")
.successForwardUrl("/login/success").
failureForwardUrl("/login/failure");

}
}
}

使用 Postman 或者其它工具进行 Post 方式的表单提交 http://localhost:8080/process?username=Felordcn&password=12345 会返回用户信息:

1
2
3
4
5
6
7
8
9
10
11
{
"httpStatus": 200,
"data": {
"userId": 1,
"username": "Felordcn",
"encodePassword": "[PROTECT]",
"age": 18
},
"msg": "登录成功",
"identifier": ""
}

把密码修改为其它值再次请求认证失败后:

1
2
3
4
5
6
{
"httpStatus": 401,
"data": null,
"msg": "登录失败了,老哥",
"identifier": "-9999"
}

6. 多种登录方式并存的实现

就这么完了了么?现在登录的花样繁多。常规的就有短信、邮箱、扫码 ,第三方是以后我要讲的不在今天范围之内。 如何应对想法多的产品经理? 我们来搞一个可扩展各种姿势的登录方式。我们在上面 2. form 登录的流程 中的 用户判定 之间增加一个适配器来适配即可。 我们知道这个所谓的判定就是UsernamePasswordAuthenticationFilter

我们只需要保证 uri 为上面配置的/process 并且能够通过 getParameter(String name) 获取用户名和密码即可

我突然觉得可以模仿 DelegatingPasswordEncoder 的搞法, 维护一个注册表执行不同的处理策略。当然我们要实现一个 GenericFilterBeanUsernamePasswordAuthenticationFilter 之前执行。同时制定登录的策略。

6.1 登录方式定义

定义登录方式枚举

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public enum LoginTypeEnum {

/**
* 原始登录方式.
*/
FORM,
/**
* Json 提交.
*/
JSON,
/**
* 验证码.
*/
CAPTCHA

}

6.2 定义前置处理器接口

定义前置处理器接口用来处理接收的各种特色的登录参数 并处理具体的逻辑。这个借口其实有点随意 ,重要的是你要学会思路。我实现了一个 默认的 form 表单登录 和 通过RequestBody放入json 的两种方式,篇幅限制这里就不展示了。具体的 DEMO 参见底部。

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
public interface LoginPostProcessor {



/**
* 获取 登录类型
*
* @return the type
*/
LoginTypeEnum getLoginTypeEnum();

/**
* 获取用户名
*
* @param request the request
* @return the string
*/
String obtainUsername(ServletRequest request);

/**
* 获取密码
*
* @param request the request
* @return the string
*/
String obtainPassword(ServletRequest request);

}

6.3 实现登录前置处理过滤器

该过滤器维护了 LoginPostProcessor 映射表。 通过前端来判定登录方式进行策略上的预处理,最终还是会交给 UsernamePasswordAuthenticationFilter 。通过 HttpSecurityaddFilterBefore(preLoginFilter, UsernamePasswordAuthenticationFilter.class)方法进行前置。

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
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
package cn.felord.spring.security.filter;

import cn.felord.spring.security.enumation.LoginTypeEnum;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.security.web.util.matcher.RequestMatcher;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import org.springframework.web.filter.GenericFilterBean;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;

import static org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter.SPRING_SECURITY_FORM_PASSWORD_KEY;
import static org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter.SPRING_SECURITY_FORM_USERNAME_KEY;

/**
* 预登录控制器
*
* @author Felordcn
* @since 16 :21 2019/10/17
*/
public class PreLoginFilter extends GenericFilterBean {


private static final String LOGIN_TYPE_KEY = "login_type";


private RequestMatcher requiresAuthenticationRequestMatcher;
private Map<LoginTypeEnum, LoginPostProcessor> processors = new HashMap<>();


public PreLoginFilter(String loginProcessingUrl, Collection<LoginPostProcessor> loginPostProcessors) {
Assert.notNull(loginProcessingUrl, "loginProcessingUrl must not be null");
requiresAuthenticationRequestMatcher = new AntPathRequestMatcher(loginProcessingUrl, "POST");
LoginPostProcessor loginPostProcessor = defaultLoginPostProcessor();
processors.put(loginPostProcessor.getLoginTypeEnum(), loginPostProcessor);

if (!CollectionUtils.isEmpty(loginPostProcessors)) {
loginPostProcessors.forEach(element -> processors.put(element.getLoginTypeEnum(), element));
}

}


private LoginTypeEnum getTypeFromReq(ServletRequest request) {
String parameter = request.getParameter(LOGIN_TYPE_KEY);

int i = Integer.parseInt(parameter);
LoginTypeEnum[] values = LoginTypeEnum.values();
return values[i];
}


/**
* 默认还是Form .
*
* @return the login post processor
*/
private LoginPostProcessor defaultLoginPostProcessor() {
return new LoginPostProcessor() {


@Override
public LoginTypeEnum getLoginTypeEnum() {

return LoginTypeEnum.FORM;
}

@Override
public String obtainUsername(ServletRequest request) {
return request.getParameter(SPRING_SECURITY_FORM_USERNAME_KEY);
}

@Override
public String obtainPassword(ServletRequest request) {
return request.getParameter(SPRING_SECURITY_FORM_PASSWORD_KEY);
}
};
}


@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
ParameterRequestWrapper parameterRequestWrapper = new ParameterRequestWrapper((HttpServletRequest) request);
if (requiresAuthenticationRequestMatcher.matches((HttpServletRequest) request)) {

LoginTypeEnum typeFromReq = getTypeFromReq(request);

LoginPostProcessor loginPostProcessor = processors.get(typeFromReq);


String username = loginPostProcessor.obtainUsername(request);

String password = loginPostProcessor.obtainPassword(request);


parameterRequestWrapper.setAttribute(SPRING_SECURITY_FORM_USERNAME_KEY, username);
parameterRequestWrapper.setAttribute(SPRING_SECURITY_FORM_PASSWORD_KEY, password);

}

chain.doFilter(parameterRequestWrapper, response);


}
}

6.4 验证

通过POST表单提交方式http://localhost:8080/process?username=Felordcn&password=12345&login_type=0可以请求成功。或者以下列方式也可以提交成功:

更多的方式 只需要实现接口 LoginPostProcessor 注入 PreLoginFilter

7. 总结

今天我们通过各种技术的运用实现了从简单登录到可动态扩展的多种方式并存的实战运用,其实我们还间接实现了前后端分离的接口登录方式。相信对你来说会有不小的收获。代码在day03分支

转载自@felord.cn

1. 前言

今天我们要进一步的的学习如何自定义配置 Spring Security 我们已经多次提到了 WebSecurityConfigurerAdapter ,而且我们知道 Spring Boot 中的自动配置实际上是通过自动配置包下的 SecurityAutoConfiguration 总配置类上导入的 Spring Boot Web 安全配置类 SpringBootWebSecurityConfiguration 来配置的。所以我们就拿它开刀

2. 自定义 Spring Boot Web 安全配置类

我们使用我们最擅长的 Ctrl + CCtrl + V 抄源码中的 SpringBootWebSecurityConfiguration ,命名为我们自定义的 CustomSpringBootWebSecurityConfiguration :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Configuration
@ConditionalOnClass(WebSecurityConfigurerAdapter.class)
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET)
public class CustomSpringBootWebSecurityConfiguration {

@Configuration
@Order(SecurityProperties.BASIC_AUTH_ORDER)
static class DefaultConfigurerAdapter extends WebSecurityConfigurerAdapter {
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
super.configure(auth);
}

@Override
public void configure(WebSecurity web) throws Exception {
super.configure(web);
}

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

相信已经有人注意到了上面 DefaultConfigurerAdapter 中我覆写(@Override)了三个方法,我们一般会通过自定义配置这三个方法来自定义我们的安全访问策略。

2.1 认证管理器配置方法

void configure(AuthenticationManagerBuilder auth) 用来配置认证管理器AuthenticationManager。说白了就是所有 UserDetails 相关的它都管,包含 PasswordEncoder 密码机。本文对 AuthenticationManager 不做具体分析讲解,后面会有专门的文章来讲这个东西。

2.2 核心过滤器配置方法

void configure(WebSecurity web) 用来配置 WebSecurity 。而 WebSecurity 是基于 Servlet Filter 用来配置 springSecurityFilterChain 。而 springSecurityFilterChain 又被委托给了 Spring Security 核心过滤器 Bean DelegatingFilterProxy 。 相关逻辑你可以在 WebSecurityConfiguration 中找到。我们一般不会过多来自定义 WebSecurity , 使用较多的是其ignoring() 方法用来忽略 Spring Security 对静态资源的控制。

2.3 安全过滤器链配置方法

void configure(HttpSecurity http) 这个是我们使用最多的,用来配置 HttpSecurity 。 HttpSecurity 用于构建一个安全过滤器链 SecurityFilterChainSecurityFilterChain 最终被注入核心过滤器HttpSecurity 有许多我们需要的配置。我们可以通过它来进行自定义安全访问策略。所以我们单独开一章来讲解这个东西。

3. HttpSecurity 配置

HttpSecurity 是后面几篇文章的重点,我们将实际操作它来实现一些实用功能。所以本文要着重介绍它。

3.1 默认配置

1
2
3
4
5
6
7
8
9
10
protected void configure(HttpSecurity http) throws Exception {
logger.debug("Using default configure(HttpSecurity). If subclassed this will potentially override subclass configure(HttpSecurity).");

http
.authorizeRequests()
.anyRequest().authenticated()
.and()
.formLogin().and()
.httpBasic();
}

上面是 Spring SecuritySpring Boot 中的默认配置。通过以上的配置,你的应用具备了一下的功能:

  • 所有的请求访问都需要被授权。
  • 使用form表单进行登陆(默认路径为/login),也就是前几篇我们见到的登录页。
  • 防止CSRF攻击、 XSS 攻击。
  • 启用 HTTP Basic 认证

3.2 常用方法解读

HttpSecurity使用了builder的构建方式来灵活制定访问策略。最早基于 XML 标签对 HttpSecurity 进行配置。现在大部分使用javaConfig方式。常用的方法解读如下:

方法 说明
openidLogin() 用于基于 OpenId 的验证
headers() 将安全标头添加到响应,比如说简单的 XSS 保护
cors() 配置跨域资源共享( CORS )
sessionManagement() 允许配置会话管理
portMapper() 允许配置一个PortMapper(HttpSecurity#(getSharedObject(class))),其他提供SecurityConfigurer的对象使用 PortMapper 从 HTTP 重定向到 HTTPS 或者从 HTTPS 重定向到 HTTP。默认情况下,Spring Security使用一个PortMapperImpl映射 HTTP 端口8080到 HTTPS 端口443,HTTP 端口80到 HTTPS 端口443
jee() 配置基于容器的预认证。 在这种情况下,认证由Servlet容器管理
x509() 配置基于x509的认证
rememberMe 允许配置“记住我”的验证
authorizeRequests() 允许基于使用HttpServletRequest限制访问
requestCache() 允许配置请求缓存
exceptionHandling() 允许配置错误处理
securityContext() 在HttpServletRequests之间的SecurityContextHolder上设置SecurityContext的管理。 当使用WebSecurityConfigurerAdapter时,这将自动应用
servletApi() 将HttpServletRequest方法与在其上找到的值集成到SecurityContext中。 当使用WebSecurityConfigurerAdapter时,这将自动应用
csrf() 添加 CSRF 支持,使用WebSecurityConfigurerAdapter时,默认启用
logout() 添加退出登录支持。当使用WebSecurityConfigurerAdapter时,这将自动应用。默认情况是,访问URL”/ logout”,使HTTP Session无效来清除用户,清除已配置的任何#rememberMe()身份验证,清除SecurityContextHolder,然后重定向到“/login?success”
anonymous() 允许配置匿名用户的表示方法。 当与WebSecurityConfigurerAdapter结合使用时,这将自动应用。 默认情况下,匿名用户将使用org.springframework.security.authentication.AnonymousAuthenticationToken表示,并包含角色 “ROLE_ANONYMOUS”
formLogin() 指定支持基于表单的身份验证。如果未指定FormLoginConfigurer#loginPage(String),则将生成默认登录页面
oauth2Login() 根据外部OAuth 2.0或OpenID Connect 1.0提供程序配置身份验证
requiresChannel() 配置通道安全。为了使该配置有用,必须提供至少一个到所需信道的映射
httpBasic() 配置 Http Basic 验证
addFilterBefore() 在指定的Filter类之前添加过滤器
addFilterAt() 在指定的Filter类的位置添加过滤器
addFilterAfter() 在指定的Filter类的之后添加过滤器
and() 连接以上策略的连接器,用来组合安全策略。实际上就是“而且”的意思

4. 总结

到今天为止,我们已经由浅入深学习了很多关于 Spring Security 的知识。已经具有开始自定义来实现一些实用的功能了,在后面的文章开始我们将结合实际开发场景进行一些实战操作

转载自@felord.cn

1. 前言

我们经常在读到一些文章会遇到uri支持Ant`风格 ,而且这个东西在Spring MVCSpring Security中经常被提及。这到底是什么呢?今天我们来学习了解一下。这对我们学习 Spring MVCSpring Security 十分必要。

2. Ant风格

说白了Ant风格就是一种路径匹配表达式。主要用来对uri的匹配。其实跟正则表达式作用是一样的,只不过正则表达式适用面更加宽泛,Ant仅仅用于路径匹配。

3. Ant 通配符

Ant中的通配符有三种:

  • ?匹配任何单个字符

  • *匹配0或者任意数量的字符

  • **匹配0或者更多的目录

这里注意了单个*是在一个目录内进行匹配。 而**是可以匹配多个目录,一定不要迷糊

3.1 Ant 通配符示例

3.2 最长匹配原则

从 3.1 可以看出 *** 是有冲突的情况存在的。为了解决这种冲突就规定了最长匹配原则(has more characters)。 一旦一个uri同时符合两个Ant匹配那么走匹配规则字符最多的。为什么走最长?因为字符越长信息越多就越具体。比如 /ant/a/path 同时满足 /**/path/ant/*/path 那么走/ant/*/path

4. Spring MVC 和 Spring Security 中的 Ant 风格

接下来我们来看看 Spring MVCSpring Security 下的Ant风格。

4.1 Spring MVC 中的 Ant 风格

这里也提一下在 Spring MVC 中 我们在控制器中写如下接口:

1
2
3
4
5
6
7
8
9
10
/**
* ant style test.
*
* @return the string
*/
@GetMapping("/?ant")
public String ant() {

return "ant";
}

你使用任意合法uri字符替代?发现都可以匹配,比如/bant 。 还有Spring MVC 的一些 过滤器注册、格式化器注册都用到了 Ant 风格。

4.2 Spring Security 中的 Ant 风格

Spring SecurityWebSecurityConfigurerAdapter 中的你可以通过如下配置进行路由权限访问控制:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class SecurityConfig extends WebSecurityConfigurerAdapter {

@Autowired
public void configureGlobal(AuthenticationManagerBuilder authenticationManagerBuilder) throws Exception {
authenticationManagerBuilder.inMemoryAuthentication().withUser("admin").password("admin").roles("USER");
}

@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
//放行静态资源 首页
.antMatchers("/index.html","/static/**").permitAll()
.anyRequest().authenticated();
}
}

上面 Spring Security 的配置中在 antMatchers 方法中通过 Ant 通配符来控制了资源的访问权限。

5. 总结

Ant 风格整体东西不多,也很好理解。 很多关于uri的配置、路由匹配、处理都用到了 Ant 风格 。对于 Web 开发人员来说是必须掌握的技能之一。

转载自@felord.cn

1. 前言

我们在前几篇对 Spring Security 的用户信息管理机制,密码机制进行了探讨。我们发现Spring Security Starter相关的Servlet自动配置都在spring-boot-autoconfigure-2.1.9.RELEASE(当前 Spring Boot 版本为2.1.9.RELEASE) 模块的路径org.springframework.boot.autoconfigure.security.servlet之下。其实官方提供的Starter组件的自动配置你都能在spring-boot-autoconfigure-2.1.9.RELEASE下找到。今天我们进一步来解密Spring SecuritySpring Boot的配置和使用。

2. Spring Boot 下 Spring Security 的自动配置

我们可以通过org.springframework.boot.autoconfigure.security.servlet路径下找到Spring Security关于Servlet的自动配置类。我们来大致了解一下。

2.1 SecurityAutoConfiguration

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
package org.springframework.boot.autoconfigure.security.servlet;

import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.security.SecurityDataConfiguration;
import org.springframework.boot.autoconfigure.security.SecurityProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.security.authentication.AuthenticationEventPublisher;
import org.springframework.security.authentication.DefaultAuthenticationEventPublisher;

/**
* {@link EnableAutoConfiguration Auto-configuration} for Spring Security.
*
* @author Dave Syer
* @author Andy Wilkinson
* @author Madhura Bhave
* @since 1.0.0
*/
@Configuration
@ConditionalOnClass(DefaultAuthenticationEventPublisher.class)
@EnableConfigurationProperties(SecurityProperties.class)
@Import({ SpringBootWebSecurityConfiguration.class, WebSecurityEnablerConfiguration.class,
SecurityDataConfiguration.class })
public class SecurityAutoConfiguration {

@Bean
@ConditionalOnMissingBean(AuthenticationEventPublisher.class)
public DefaultAuthenticationEventPublisher authenticationEventPublisher(ApplicationEventPublisher publisher) {
return new DefaultAuthenticationEventPublisher(publisher);
}

}

SecurityAutoConfiguration顾名思义安全配置类。该类引入(@import)了SpringBootWebSecurityConfigurationWebSecurityEnablerConfigurationSecurityDataConfiguration三个配置类。 让这三个模块的类生效。是一个复合配置,是 Spring Security 自动配置最重要的一个类之一。 Spring Boot 自动配置经常使用这种方式以达到灵活配置的目的,这也是我们研究 Spring Security 自动配置的一个重要入口 同时 SecurityAutoConfiguration还将DefaultAuthenticationEventPublisher作为默认的AuthenticationEventPublisher注入Spring IoC容器。如果你熟悉 Spring 中的事件机制你就会知道该类是一个 Spring 事件发布器。该类内置了一个HashMap<String, Constructor<? extends AbstractAuthenticationEvent>>维护了认证异常处理和对应异常事件处理逻辑的映射关系,比如账户过期异常AccountExpiredException对应认证过期事件AuthenticationFailureExpiredEvent ,也就是说发生不同认证的异常使用不同处理策略。

2.2 SpringBootWebSecurityConfiguration

1
2
3
4
5
6
7
8
9
10
11
12
13
@Configuration
@ConditionalOnClass(WebSecurityConfigurerAdapter.class)
@ConditionalOnMissingBean(WebSecurityConfigurerAdapter.class)
@ConditionalOnWebApplication(type = Type.SERVLET)
public class SpringBootWebSecurityConfiguration {

@Configuration
@Order(SecurityProperties.BASIC_AUTH_ORDER)
static class DefaultConfigurerAdapter extends WebSecurityConfigurerAdapter {

}

}

这个类是Spring Security 对 Spring Boot Servlet Web 应用的默认配置。核心在于WebSecurityConfigurerAdapter适配器。从@ConditionalOnMissingBean(WebSecurityConfigurerAdapter.class)我们就能看出 WebSecurityConfigurerAdapter是安全配置的核心。 默认情况下DefaultConfigurerAdapter将以SecurityProperties.BASIC_AUTH_ORDER(-5)的顺序注入 Spring IoC 容器,这是个空实现。如果我们需要个性化可以通过继承WebSecurityConfigurerAdapter来实现。我们会在以后的博文重点介绍该类。

2.3 WebSecurityEnablerConfiguration

1
2
3
4
5
6
7
8
@Configuration
@ConditionalOnBean(WebSecurityConfigurerAdapter.class)
@ConditionalOnMissingBean(name = BeanIds.SPRING_SECURITY_FILTER_CHAIN)
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET)
@EnableWebSecurity
public class WebSecurityEnablerConfiguration {

}

该配置类会在SpringBootWebSecurityConfiguration注入 Spring IoC 容器后启用@EnableWebSecurity注解。也就是说WebSecurityEnablerConfiguration目的仅仅就是在某些条件下激活@EnableWebSecurity注解。那么这个注解都有什么呢?

3. @EnableWebSecurity 注解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Retention(value = java.lang.annotation.RetentionPolicy.RUNTIME)
@Target(value = { java.lang.annotation.ElementType.TYPE })
@Documented
@Import({ WebSecurityConfiguration.class,
SpringWebMvcImportSelector.class,
OAuth2ImportSelector.class })
@EnableGlobalAuthentication
@Configuration
public @interface EnableWebSecurity {

/**
* Controls debugging support for Spring Security. Default is false.
* @return if true, enables debug support with Spring Security
*/
boolean debug() default false;
}

@Enable*这类注解都是带配置导入的注解。通过导入一些配置来启用一些特定功能。 @EnableWebSecurity导入了 WebSecurityConfigurationSpringWebMvcImportSelectorOAuth2ImportSelector以及启用了@EnableGlobalAuthentication注解。

3.1 WebSecurityConfiguration

该配置类WebSecurityConfiguration使用一个WebSecurity对象基于用户指定的或者默认的安全配置,你可以通过继承WebSecurityConfigurerAdapter或者实现WebSecurityConfigurer来定制WebSecurity创建一个FilterChainProxy Bean来对用户请求进行安全过滤。这个FilterChainProxy的名称就是WebSecurityEnablerConfiguration上BeanIds.SPRING_SECURITY_FILTER_CHAIN 也就是 springSecurityFilterChain,它是一个Filter,最终会被作为Servlet过滤器链中的一个Filter应用到Servlet容器中。安全处理的策略主要是过滤器的调用顺序。WebSecurityConfiguration最终会通过@EnableWebSecurity应用到系统。

源码分析:

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
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
package org.springframework.security.config.annotation.web.configuration;

import java.util.Collections;
import java.util.List;
import java.util.Map;

import javax.servlet.Filter;

import org.springframework.beans.factory.BeanClassLoaderAware;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.DependsOn;
import org.springframework.context.annotation.ImportAware;
import org.springframework.core.OrderComparator;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.AnnotationAttributes;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.core.annotation.Order;
import org.springframework.core.type.AnnotationMetadata;
import org.springframework.security.access.expression.SecurityExpressionHandler;
import org.springframework.security.config.annotation.ObjectPostProcessor;
import org.springframework.security.config.annotation.SecurityConfigurer;
import org.springframework.security.config.annotation.web.WebSecurityConfigurer;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.context.DelegatingApplicationListener;
import org.springframework.security.web.FilterChainProxy;
import org.springframework.security.web.FilterInvocation;
import org.springframework.security.web.access.WebInvocationPrivilegeEvaluator;
import org.springframework.security.web.context.AbstractSecurityWebApplicationInitializer;


/**
* Spring Web Security 的配置类 :
* 1. 使用一个 WebSecurity 对象基于安全配置创建一个 FilterChainProxy 对象来对用户请求进行安全过滤。
* 2. 也会暴露诸如 安全SpEL表达式处理器 SecurityExpressionHandler 等一些类。
*
* @see EnableWebSecurity
* @see WebSecurity
*
* @author Rob Winch
* @author Keesun Baik
* @since 3.2
*/
@Configuration
public class WebSecurityConfiguration implements ImportAware, BeanClassLoaderAware {
private WebSecurity webSecurity;
// 是否启用了调试模式,来自注解 @EnableWebSecurity 的属性 debug,缺省值 false
private Boolean debugEnabled;

private List<SecurityConfigurer<Filter, WebSecurity>> webSecurityConfigurers;

private ClassLoader beanClassLoader;

@Autowired(required = false)
private ObjectPostProcessor<Object> objectObjectPostProcessor;
/**
*
* 代理监听器 应该时监听 DefaultAuthenticationEventPublisher 的一些处理策略
*/
@Bean
public static DelegatingApplicationListener delegatingApplicationListener() {
return new DelegatingApplicationListener();
}
/**
*
* 安全SpEL表达式处理器 SecurityExpressionHandler 缺省为一个 DefaultWebSecurityExpressionHandler
*/
@Bean
@DependsOn(AbstractSecurityWebApplicationInitializer.DEFAULT_FILTER_NAME)
public SecurityExpressionHandler<FilterInvocation> webSecurityExpressionHandler() {
return webSecurity.getExpressionHandler();
}

/**
* Spring Security 核心过滤器 Spring Security Filter Chain , Bean ID 为 springSecurityFilterChain
* @return the {@link Filter} that represents the security filter chain
* @throws Exception
*/
@Bean(name = AbstractSecurityWebApplicationInitializer.DEFAULT_FILTER_NAME)
public Filter springSecurityFilterChain() throws Exception {
boolean hasConfigurers = webSecurityConfigurers != null
&& !webSecurityConfigurers.isEmpty();
if (!hasConfigurers) {
WebSecurityConfigurerAdapter adapter = objectObjectPostProcessor
.postProcess(new WebSecurityConfigurerAdapter() {
});
webSecurity.apply(adapter);
}
return webSecurity.build();
}

/**
*
* 用于模板 如JSP Freemarker 的一些页面标签按钮控制支持
* Creates the {@link WebInvocationPrivilegeEvaluator} that is necessary for the JSP
* tag support.
* @return the {@link WebInvocationPrivilegeEvaluator}
* @throws Exception
*/
@Bean
@DependsOn(AbstractSecurityWebApplicationInitializer.DEFAULT_FILTER_NAME)
public WebInvocationPrivilegeEvaluator privilegeEvaluator() throws Exception {
return webSecurity.getPrivilegeEvaluator();
}

/**
*
* 用于创建web configuration的SecurityConfigurer实例,
* 注意该参数通过@Value(...)方式注入,对应的bean autowiredWebSecurityConfigurersIgnoreParents
* 也在该类中定义
*
* @param objectPostProcessor the {@link ObjectPostProcessor} used to create a
* {@link WebSecurity} instance
* @param webSecurityConfigurers the
* {@code <SecurityConfigurer<FilterChainProxy, WebSecurityBuilder>} instances used to
* create the web configuration
* @throws Exception
*/
@Autowired(required = false)
public void setFilterChainProxySecurityConfigurer(
ObjectPostProcessor<Object> objectPostProcessor,
@Value("#{@autowiredWebSecurityConfigurersIgnoreParents.getWebSecurityConfigurers()}") List<SecurityConfigurer<Filter, WebSecurity>> webSecurityConfigurers)
throws Exception {
webSecurity = objectPostProcessor
.postProcess(new WebSecurity(objectPostProcessor));
if (debugEnabled != null) {
webSecurity.debug(debugEnabled);
}

Collections.sort(webSecurityConfigurers, AnnotationAwareOrderComparator.INSTANCE);

Integer previousOrder = null;
Object previousConfig = null;
for (SecurityConfigurer<Filter, WebSecurity> config : webSecurityConfigurers) {
Integer order = AnnotationAwareOrderComparator.lookupOrder(config);
if (previousOrder != null && previousOrder.equals(order)) {
throw new IllegalStateException(
"@Order on WebSecurityConfigurers must be unique. Order of "
+ order + " was already used on " + previousConfig + ", so it cannot be used on "
+ config + " too.");
}
previousOrder = order;
previousConfig = config;
}
for (SecurityConfigurer<Filter, WebSecurity> webSecurityConfigurer : webSecurityConfigurers) {
webSecurity.apply(webSecurityConfigurer);
}
this.webSecurityConfigurers = webSecurityConfigurers;
}
/**
* 从当前bean容器中获取所有的WebSecurityConfigurer bean。
* 这些WebSecurityConfigurer通常是由开发人员实现的配置类,并且继承自WebSecurityConfigurerAdapter
*
*/
@Bean
public static AutowiredWebSecurityConfigurersIgnoreParents autowiredWebSecurityConfigurersIgnoreParents(
ConfigurableListableBeanFactory beanFactory) {
return new AutowiredWebSecurityConfigurersIgnoreParents(beanFactory);
}

/**
* A custom verision of the Spring provided AnnotationAwareOrderComparator that uses
* {@link AnnotationUtils#findAnnotation(Class, Class)} to look on super class
* instances for the {@link Order} annotation.
*
* @author Rob Winch
* @since 3.2
*/
private static class AnnotationAwareOrderComparator extends OrderComparator {
private static final AnnotationAwareOrderComparator INSTANCE = new AnnotationAwareOrderComparator();

@Override
protected int getOrder(Object obj) {
return lookupOrder(obj);
}

private static int lookupOrder(Object obj) {
if (obj instanceof Ordered) {
return ((Ordered) obj).getOrder();
}
if (obj != null) {
Class<?> clazz = (obj instanceof Class ? (Class<?>) obj : obj.getClass());
Order order = AnnotationUtils.findAnnotation(clazz, Order.class);
if (order != null) {
return order.value();
}
}
return Ordered.LOWEST_PRECEDENCE;
}
}

/*
* 要是为了获取注解 @EnableWebSecurity 的属性 debugEnabled
*
* @see org.springframework.context.annotation.ImportAware#setImportMetadata(org.
* springframework.core.type.AnnotationMetadata)
*/
public void setImportMetadata(AnnotationMetadata importMetadata) {
Map<String, Object> enableWebSecurityAttrMap = importMetadata
.getAnnotationAttributes(EnableWebSecurity.class.getName());
AnnotationAttributes enableWebSecurityAttrs = AnnotationAttributes
.fromMap(enableWebSecurityAttrMap);
debugEnabled = enableWebSecurityAttrs.getBoolean("debug");
if (webSecurity != null) {
webSecurity.debug(debugEnabled);
}
}

/*
* (non-Javadoc)
*
* @see
* org.springframework.beans.factory.BeanClassLoaderAware#setBeanClassLoader(java.
* lang.ClassLoader)
*/
public void setBeanClassLoader(ClassLoader classLoader) {
this.beanClassLoader = classLoader;
}
}

3.2 SpringWebMvcImportSelector

该类是为了对 Spring Mvc 进行支持的。一旦发现应用使用 Spring Mvc 的核心前置控制器DispatcherServlet就会引入WebMvcSecurityConfiguration。主要是为了适配 Spring Mvc 。

3.3 OAuth2ImportSelector

该类是为了对OAuth2.0开放授权协议进行支持。ClientRegistration如果被引用,具体点也就是spring-security-oauth2模块被启用(引入依赖jar)时。会启用OAuth2客户端配置OAuth2ClientConfiguration

3.4 @EnableGlobalAuthentication

这个类主要引入了AuthenticationConfiguration目的主要为了构造 认证管理器AuthenticationManagerAuthenticationManager十分重要后面我们会进行专门的分析。

4. SecurityFilterAutoConfiguration

我们在org.springframework.boot.autoconfigure.security.servlet路径下还发现了一个配置类SecurityFilterAutoConfiguration。该类用于向Servlet容器注册一个名称为securityFilterChainRegistration的bean, 实现类是DelegatingFilterProxyRegistrationBean。该 bean 的目的是注册另外一个Servlet Filter BeanServlet 容器,实现类为DelegatingFilterProxyDelegatingFilterProxy其实是一个代理过滤器,它被Servlet 容器用于处理请求时,会将任务委托给指定给自己另外一个Filter bean。对于SecurityFilterAutoConfiguration,来讲,这个被代理的Filter bean的名字为springSecurityFilterChain, 也就是我们上面提到过的 Spring Security Web提供的用于请求安全处理的Filter bean,其实现类是FilterChainProxy

相关的源码分析:

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
package org.springframework.boot.autoconfigure.security.servlet;

import java.util.EnumSet;
import java.util.stream.Collectors;

import javax.servlet.DispatcherType;

import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication.Type;
import org.springframework.boot.autoconfigure.security.SecurityProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.web.servlet.DelegatingFilterProxyRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfiguration;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.context.AbstractSecurityWebApplicationInitializer;

@Configuration
// 仅在 Servlet 环境下生效
@ConditionalOnWebApplication(type = Type.SERVLET)
// 确保安全属性配置信息被加载并以bean形式被注册到容器
@EnableConfigurationProperties(SecurityProperties.class)
// 仅在特定类存在于 classpath 上时才生效
@ConditionalOnClass({ AbstractSecurityWebApplicationInitializer.class,
SessionCreationPolicy.class })
// 指定该配置类在 SecurityAutoConfiguration 配置类应用之后应用
@AutoConfigureAfter(SecurityAutoConfiguration.class)
public class SecurityFilterAutoConfiguration {

// 要注册到 Servlet 容器的 DelegatingFilterProxy Filter的
// 目标代理Filter bean的名称 :springSecurityFilterChain
private static final String DEFAULT_FILTER_NAME =
AbstractSecurityWebApplicationInitializer.DEFAULT_FILTER_NAME;


// 定义一个 bean securityFilterChainRegistration,
// 该 bean 的目的是注册另外一个 bean 到 Servlet 容器 : 实现类为 DelegatingFilterProxy 的一个 Servlet Filter
// 该 DelegatingFilterProxy Filter 其实是一个代理过滤器,它被 Servlet 容器用于匹配特定URL模式的请求,
// 而它会将任务委托给指定给自己的名字为 springSecurityFilterChain 的 Filter, 也就是 Spring Security Web
// 提供的用于请求安全处理的一个 Filter bean,其实现类是 FilterChainProxy
// (可以将 1 个 FilterChainProxy 理解为 1 HttpFirewall + n SecurityFilterChain)
@Bean
@ConditionalOnBean(name = DEFAULT_FILTER_NAME)
public DelegatingFilterProxyRegistrationBean securityFilterChainRegistration(
SecurityProperties securityProperties) {
DelegatingFilterProxyRegistrationBean registration = new DelegatingFilterProxyRegistrationBean(
DEFAULT_FILTER_NAME);
registration.setOrder(securityProperties.getFilter().getOrder());
registration.setDispatcherTypes(getDispatcherTypes(securityProperties));
return registration;
}

private EnumSet<DispatcherType> getDispatcherTypes(
SecurityProperties securityProperties) {
if (securityProperties.getFilter().getDispatcherTypes() == null) {
return null;
}
return securityProperties.getFilter().getDispatcherTypes().stream()
.map((type) -> DispatcherType.valueOf(type.name())).collect(Collectors
.collectingAndThen(Collectors.toSet(), EnumSet::copyOf));
}

}

5. 总结

本文主要对 Spring Security 在 Spring Boot 中的自动配置一些机制进行了粗略的讲解。为什么没有细讲。因为从学习出发有些东西不是我们必须要深入了解的,但是又要知道一点点相关的知识。我们先宏观上有个大致的了解就行。所以在阅读本文一定不要钻牛角尖。粗略知道配置策略、加载策略和一些关键类的作用即可。在你对 Spring Security 有了进一步学习之后,回头认真来看这些配置类会有更深层的思考 从另一个方面该文也给你阅读 Spring 源码提供了一些思路,学会这些才是最重要的。

转载自@felord.cn

1. 前言

我们对Spring Security中的重要用户信息主体UserDetails进行了探讨。中间例子我们使用了明文密码,规则是通过对密码明文添加{noop}前缀。那么本节将对Spring Security中的密码编码进行一些探讨。

2. 不推荐使用md5

首先md5不是加密算法,是哈希摘要。以前通常使用其作为密码哈希来保护密码。由于彩虹表的出现,md5sha1之类的摘要算法都已经不安全了。如果有不相信的同学 可以到一些解密网站 如 cmd5 网站尝试解密,你会发现md5sha1是真的非常容易被破解。

3. Spring Security中的密码算法

上文我们提到了InMemoryUserDetailsManager初始化Bean 需要传输一个ObjectProvider<PasswordEncoder>参数。这里的PasswordEncoder就是我们对密码进行编码的工具接口。该接口只有两个功能:一个是匹配验证。另一个是密码编码

上图就是Spring Security 提供的org.springframework.security.crypto.password.PasswordEncoder一些实现,有的已经过时。其中我们注意到一个叫委托密码编码器的实现

3.1 委托密码编码器 DelegatingPasswordEncoder

什么是委托(Delegate)? 就是甲方交给乙方的活。乙方呢手里又很多的渠道,但是乙方光想赚差价又不想干活。所以乙方根据一些规则又把活委托给了别人,让别人来干。这里的乙方就是DelegatingPasswordEncoder。该类维护了以下清单:

  • final String idForEncode通过id来匹配编码器,该id不能是{} 包括的。DelegatingPasswordEncoder初始化传入,用来提供默认的密码编码器。
  • final PasswordEncoder passwordEncoderForEncode通过上面idForEncode所匹配到的PasswordEncoder用来对密码进行编码
  • final Map<String, PasswordEncoder> idToPasswordEncoder用来维护多个idForEncode与具体PasswordEncoder的映射关系。DelegatingPasswordEncoder初始化时装载进去,会在初始化时进行一些规则校验。
  • PasswordEncoder defaultPasswordEncoderForMatches = new UnmappedIdPasswordEncode默认的密码匹配器,上面的Map中都不存在就用它来执行matches方法进行匹配验证。这是一个内部类实现。

DelegatingPasswordEncoder编码方法:

1
2
3
4
@Override
public String encode(CharSequence rawPassword) {
return PREFIX + this.idForEncode + SUFFIX + this.passwordEncoderForEncode.encode(rawPassword);
}

从上面源码可以看出来通过DelegatingPasswordEncoder编码后的密码是遵循一定的规则的,遵循``{idForEncode}encodePassword。也就是前缀{}`包含了编码的方式再拼接上该方式编码后的密码串。

DelegatingPasswordEncoder密码匹配方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Override
public boolean matches(CharSequence rawPassword, String prefixEncodedPassword) {
if (rawPassword == null && prefixEncodedPassword == null) {
return true;
}
String id = extractId(prefixEncodedPassword);
PasswordEncoder delegate = this.idToPasswordEncoder.get(id);
if (delegate == null) {
return this.defaultPasswordEncoderForMatches
.matches(rawPassword, prefixEncodedPassword);
}
String encodedPassword = extractEncodedPassword(prefixEncodedPassword);
return delegate.matches(rawPassword, encodedPassword);
}

密码匹配通过传入原始密码和遵循{idForEncode}encodePassword规则的密码编码串。通过获取编码方式id(idForEncode)来从DelegatingPasswordEncoder中的映射集合idToPasswordEncoder中获取具体的PasswordEncoder进行匹配校验。找不到就使用UnmappedIdPasswordEncoder

这就是DelegatingPasswordEncoder的工作流程。那么DelegatingPasswordEncoder在哪里实例化呢?

3.2 密码器静态工厂PasswordEncoderFactories

从名字上就看得出来这是个工厂啊,专门制造PasswordEncoder。而且还是个静态工厂只提供了初始化DelegatingPasswordEncoder的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@SuppressWarnings("deprecation")
public static PasswordEncoder createDelegatingPasswordEncoder() {
String encodingId = "bcrypt";
Map<String, PasswordEncoder> encoders = new HashMap<>();
encoders.put(encodingId, new BCryptPasswordEncoder());
encoders.put("ldap", new org.springframework.security.crypto.password.LdapShaPasswordEncoder());
encoders.put("MD4", new org.springframework.security.crypto.password.Md4PasswordEncoder());
encoders.put("MD5", new org.springframework.security.crypto.password.MessageDigestPasswordEncoder("MD5"));
encoders.put("noop", org.springframework.security.crypto.password.NoOpPasswordEncoder.getInstance());
encoders.put("pbkdf2", new Pbkdf2PasswordEncoder());
encoders.put("scrypt", new SCryptPasswordEncoder());
encoders.put("SHA-1", new org.springframework.security.crypto.password.MessageDigestPasswordEncoder("SHA-1"));
encoders.put("SHA-256", new org.springframework.security.crypto.password.MessageDigestPasswordEncoder("SHA-256"));
encoders.put("sha256", new org.springframework.security.crypto.password.StandardPasswordEncoder());

return new DelegatingPasswordEncoder(encodingId, encoders);
}

从上面可以非常具体地看出来DelegatingPasswordEncoder提供的密码编码方式。默认采用了bcrypt进行编码。我们可终于明白了为什么上一文中我们使用{noop}12345能和我们前台输入的12345匹配上。这么搞有什么好处呢?这可以实现一个场景,如果有一天我们对密码编码规则进行替换或者轮转。现有的用户不会受到影响。 那么Spring Security 是如何配置密码编码器PasswordEncoder呢?

4. Spring Security 加载 PasswordEncoder 的规则

我们在Spring Security配置适配器WebSecurityConfigurerAdapter(该类我以后的文章会仔细分析 可通过https://felord.cn 来及时获取相关信息)找到了引用PasswordEncoderFactories的地方,一个内部PasswordEncoder实现LazyPasswordEncoder。从源码上看该类是懒加载的只有用到了才去实例化。在该类的内部方法中发现了PasswordEncoder的规则。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 获取最终干活的PasswordEncoder
private PasswordEncoder getPasswordEncoder() {
if (this.passwordEncoder != null) {
return this.passwordEncoder;
}
PasswordEncoder passwordEncoder = getBeanOrNull(PasswordEncoder.class);
if (passwordEncoder == null) {
passwordEncoder = PasswordEncoderFactories.createDelegatingPasswordEncoder();
}
this.passwordEncoder = passwordEncoder;
return passwordEncoder;
}
// 从Spring IoC容器中获取Bean 有可能获取不到
private <T> T getBeanOrNull(Class<T> type) {
try {
return this.applicationContext.getBean(type);
} catch(NoSuchBeanDefinitionException notFound) {
return null;
}
}

上面的两个方法总结:如果能从从Spring IoC容器中获取PasswordEncoder的Bean就用该Bean作为编码器,没有就使用DelegatingPasswordEncoder。默认是bcrypt方式。文中多次提到该算法。而且还是Spring Security默认的。那么它到底是什么呢?

5. bcrypt 编码算法

这里简单提一下bcryptbcrypt使用的是布鲁斯·施内尔在1993年发布的Blowfish加密算法。bcrypt算法将salt随机并混入最终加密后的密码,验证时也无需单独提供之前的salt,从而无需单独处理salt问题。加密后的格式一般为:

1
$2a$10$/bTVvqqlH9UiE0ZJZ7N2Me3RIgUCdgMheyTgV0B4cMCSokPa.6oCa

其中:$是分割符,无意义;2abcrypt加密版本号;10cost的值;而后的前22位是salt值;再然后的字符串就是密码的密文了。

5.1 bcrypt 特点

. bcrypt有个特点就是非常慢。这大大提高了使用彩虹表进行破解的难度。也就是说该类型的密码暗文拥有让破解者无法忍受的时间成本。同时对于开发者来说也需要注意该时长是否能超出系统忍受范围内。通常是MD5的数千倍。
. 同样的密码每次使用bcrypt编码,密码暗文都是不一样的。 也就是说你有两个网站如果都使用了bcrypt它们的暗文是不一样的,这不会因为一个网站泄露密码暗文而使另一个网站也泄露密码暗文。

所以从bcrypt的特点上来看,其安全强度还是非常有保证的

6. 总结

今天我们对Spring Security中的密码编码进行分析。发现了默认情况下使用bcrypt进行编码。而密码验证匹配则通过密码暗文前缀中的加密方式id控制。你也可以向Spring IoC容器注入一个PasswordEncoder类型的Bean 来达到自定义的目的。我们还对bcrypt算法进行一些简单了解,对其特点进行了总结。后面我们会Spring Security进行进一步学习。关于上一篇文章的demo我也已经替换成了数据库管理用户,相关代码请使用day02分支获取

转载自@felord.cn

1.前言

本篇将通过Spring Boot 2.x来讲解Spring Security中的用户主题UserDetails

2.Spring Boot集成Spring Security

集成Spring Security只需要引入其对应的Starter组件。Spring Security不仅仅能保Servlet Web应用,也可以保护Reactive Web应用,本文我们讲前者。我们只需要在Spring Security项目引入以下依赖即可:

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
<dependencies>
<!-- actuator 指标监控 非必须 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!-- spring security starter 必须 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- spring mvc servlet web 必须 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- lombok 插件 非必须 -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!-- 测试 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

3.UserDetailsServiceAutoConfiguration

启动项目,访问Actuator端点http://localhost:8080/actuator会跳转到一个登录页面,如下:

登录页面

要求你输入用户名Username(默认值为user)和密码Password。密码在springboot控制台会打印出类似Using generated security password: e1f163be-ad18-4be1-977c-88a6bcee0d37的字样,后面的长串就是密码,当然这不是生产可用的。如果你足够细心会从控制台打印日志发现该随机密码是由UserDetailsServiceAutoConfiguration配置类生成的,我们就从它开始顺藤摸瓜来一探究竟。

3.1 UserDetailsService

UserDetailsService接口只提供了一个方法

1
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;

该方法很容易理解:通过用户名来加载用户。这个方法主要用于从系统数据中查询并加载具体的用户到Spring Security中。

3.2 UserDetails

从上面UserDetailsService可以知道最终交给Spring Security的是UserDetails。该接口是提供用户信息的核心接口。该接口实现仅仅存储用户信息。后续会将该接口提供的用户信息封装到认证对象Authentication中取。UserDetails默认提供了:

. 用户的权限集,默认需要添加ROLE_前缀
. 用户的加密后的密码,不加密会使用{noop}前缀
. 应用内唯一的用户名
. 账户是否过期
. 账户是否锁定
. 凭证是否过期
. 用户是否可用

如果以上的信息满足不了你使用,你可以自行实现扩展以存储更多的用户信息。比如用户的邮箱、手机号等等。通常我们使用其实现类:

1
org.springframework.security.core.userdetails.User

该类内置一个建造起UserBuilder会很方便地帮助我们构建UserDetails对象,后面我们会用到它

3.3 UserDetailsServiceAutoConfiguration

UserDetailsServiceAutoconfiguration全限定名为:

1
org.springframework.boot.autoconfigure.security.servlet.UserDetailsServiceAutoConfiguration

源码如下:

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
@Configuration
@ConditionalOnClass(AuthenticationManager.class)
@ConditionalOnBean(ObjectPostProcessor.class)
@ConditionalOnMissingBean({ AuthenticationManager.class, AuthenticationProvider.class, UserDetailsService.class })
public class UserDetailsServiceAutoConfiguration {

private static final String NOOP_PASSWORD_PREFIX = "{noop}";

private static final Pattern PASSWORD_ALGORITHM_PATTERN = Pattern.compile("^\\{.+}.*$");

private static final Log logger = LogFactory.getLog(UserDetailsServiceAutoConfiguration.class);

@Bean
@ConditionalOnMissingBean(
type = "org.springframework.security.oauth2.client.registration.ClientRegistrationRepository")
@Lazy
public InMemoryUserDetailsManager inMemoryUserDetailsManager(SecurityProperties properties,
ObjectProvider<PasswordEncoder> passwordEncoder) {
SecurityProperties.User user = properties.getUser();
List<String> roles = user.getRoles();
return new InMemoryUserDetailsManager(
User.withUsername(user.getName()).password(getOrDeducePassword(user, passwordEncoder.getIfAvailable()))
.roles(StringUtils.toStringArray(roles)).build());
}

private String getOrDeducePassword(SecurityProperties.User user, PasswordEncoder encoder) {
String password = user.getPassword();
if (user.isPasswordGenerated()) {
logger.info(String.format("%n%nUsing generated security password: %s%n", user.getPassword()));
}
if (encoder != null || PASSWORD_ALGORITHM_PATTERN.matcher(password).matches()) {
return password;
}
return NOOP_PASSWORD_PREFIX + password;
}

}

我们来简单解读一下该类,从@Conditional系列注解我们知道该类在类路径下存在AuthenticationManager、在Spring容器中存在BeanObjectPostProcessor并且不存在BeanAuthenticationManagerAuthenticationProviderUserDetailsService的情况下生效。千万不要纠结这些类干嘛用的该类只初始化了一个UserDetailsManager类型的Bean。UserDetailsManager类负责对安全用户实体抽象UserDetails的增删改查操作。同时还继承了UserDetailsService接口。

明白了上面这些让我们把目光再回到UserDetailsServiceAutoConfiguration上来。该类初始化了一个名为InMemoryUserDetailsManager的内存用户管理器。该管理器通过配置注入了一个默认的UserDetails存在内存中,就是我们上面用的那个user,每次启动user都是动态生成的。那么问题来了如果我们定义自己的UserDetailsManagerBean是不是就可以实现我们需要的用户管理逻辑呢?

3.4 自定义UserDetailsManager

我们来自定义一个UserDetailsManager来看看能不能达到自定义用户管理的效果。首先我们针对UserDetailsManager的所有方法进行一个代理实现,我们依然将用户存在内存中,区别就是这是我们自定义的:

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
package cn.felord.spring.security;

import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UsernameNotFoundException;

import java.util.HashMap;
import java.util.Map;

/**
* 代理 {@link org.springframework.security.provisioning.UserDetailsManager} 所有功能
*
* @author Felordcn
*/
public class UserDetailsRepository {

private Map<String, UserDetails> users = new HashMap<>();


public void createUser(UserDetails user) {
users.putIfAbsent(user.getUsername(), user);
}


public void updateUser(UserDetails user) {
users.put(user.getUsername(), user);
}


public void deleteUser(String username) {
users.remove(username);
}


public void changePassword(String oldPassword, String newPassword) {
Authentication currentUser = SecurityContextHolder.getContext()
.getAuthentication();

if (currentUser == null) {
// This would indicate bad coding somewhere
throw new AccessDeniedException(
"Can't change password as no Authentication object found in context "
+ "for current user.");
}

String username = currentUser.getName();

UserDetails user = users.get(username);


if (user == null) {
throw new IllegalStateException("Current user doesn't exist in database.");
}

// todo copy InMemoryUserDetailsManager 自行实现具体的更新密码逻辑
}


public boolean userExists(String username) {

return users.containsKey(username);
}


public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
return users.get(username);
}
}

该类负责具体对UserDetails的增删改查操作。我们将其注入Spring容器:

1
2
3
4
5
6
7
8
9
@Bean
public UserDetailsRepository userDetailsRepository() {
UserDetailsRepository userDetailsRepository = new UserDetailsRepository();

// 为了让我们的登录能够运行 这里我们初始化一个用户Felordcn 密码采用明文 当你在密码12345上使用了前缀{noop} 意味着你的密码不使用加密,authorities 一定不能为空 这代表用户的角色权限集合
UserDetails felordcn = User.withUsername("Felordcn").password("{noop}12345").authorities(AuthorityUtils.NO_AUTHORITIES).build();
userDetailsRepository.createUser(felordcn);
return userDetailsRepository;
}

为了方便测试,我们也内置一个名称为Felordcn密码为12345UserDetails用户,密码采用明文。当你在密码12345上使用了前缀{noop}意味着你的密码不使用加密,这里我们并没有指定密码加密方式你可以使用PasswordEncoder来指定一种加密方式。通常推荐使用Bcrypt作为加密方式。默认Spring Security使用的也是此方式。authorities一定不能为null这代表用户的角色权限集合。接下来我们实现一个UserDetailsManager并注入Spring 容器:

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
@Bean
public UserDetailsManager userDetailsManager(UserDetailsRepository userDetailsRepository) {
return new UserDetailsManager() {
@Override
public void createUser(UserDetails user) {
userDetailsRepository.createUser(user);
}

@Override
public void updateUser(UserDetails user) {
userDetailsRepository.updateUser(user);
}

@Override
public void deleteUser(String username) {
userDetailsRepository.deleteUser(username);
}

@Override
public void changePassword(String oldPassword, String newPassword) {
userDetailsRepository.changePassword(oldPassword, newPassword);
}

@Override
public boolean userExists(String username) {
return userDetailsRepository.userExists(username);
}

@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
return userDetailsRepository.loadUserByUsername(username);
}
};
}

这样实际执行委托给了UserDetailsRepository来做。我们重复章节3.的动作进入登录页面分别输入Felordcn12345成功进入。

3.5 数据库管理用户

经过以上的配置,相信聪明的你已经知道如何使用数据库来管理用户了。只需要将UserDetailsRepository中的users属性替代为抽象的Dao接口就行了,无论你使用JPA还是Mybatis来实现。

4. 总结

今天我们对Spring Security 中的用户信息 UserDetails 相关进行的一些解读。并自定义了用户信息处理服务。相信你已经对在Spring Security中如何加载用户信息,如何扩展用户信息有所掌握了源码地址,使用day01分支

转载自@felord.cn

pic

浏览器缓存是前端开发中不可避免的问题,对于web应用来说,它是提升页面性能同时减少服务器压力的利器。本文将简单地描述总结下浏览器缓存的知识和应用,希望对自己和大家都有所帮助

浏览器缓存类型

  • 强缓存
    不会向服务器发送请求,直接从缓存中读取资源,在chrome控制台的network选项中可以看到该请求返回200的状态码,并且size显示from disk cache或from memory cache

  • 协商缓存
    向服务器发送请求,服务器会根据这个请求的request header的一些参数来判断是否命中协商缓存,如果命中,则返回304状态码并带上新的response header通知浏览器从缓存中读取资源

两者的共同点是,都是从客户端缓存中读取资源;区别是强缓存不会发请求,协商缓存会发请求

缓存有关的header

强缓存

Expires:response header里的过期时间,浏览器再次加载资源时,如果在这个过期时间内,则命中强缓存

Cache-Control:当值设为max-age=300时,则代表在这个请求正确返回时间(浏览器也会记录下来)的5分钟内再次加载资源,就会命中强缓存

cache-control-expire.png

Expires和Cache-Control:max-age=*** 的作用是差不多的,区别就在于 Expires 是http1.0的产物,Cache-Control是http1.1的产物,两者同时存在的话,Cache-Control优先级高于Expires;在某些不支持HTTP1.1的环境下,Expires就会发挥用处。所以Expires其实是过时的产物,现阶段它的存在只是一种兼容性的写法

Expires和Cache-Control的区别还有一个:Expires是一个具体的服务器时间,这就导致一个问题,如果客户端时间和服务器时间相差较大,缓存命中与否就不是开发者所期望的。Cache-Control是一个时间段,控制就比较容易

协商缓存

ETag和If-None-Match:这两个要一起说。Etag是上一次加载资源时,服务器返回的response header,是对该资源的一种唯一标识,只要资源有变化,Etag就会重新生成。浏览器在下一次加载资源向服务器发送请求时,会将上一次返回的Etag值放到request header里的If-None-Match里,服务器接受到If-None-Match的值后,会拿来跟该资源文件的Etag值做比较,如果相同,则表示资源文件没有发生改变,命中协商缓存

Last-Modified和If-Modified-Since:这两个也要一起说。Last-Modified是该资源文件最后一次更改时间,服务器会在response header里返回,同时浏览器会将这个值保存起来,在下一次发送请求时,放到request header里的If-Modified-Since里,服务器在接收到后也会做比对,如果相同则命中协商缓存

response header

etag-lastmodified.png

request header

if-none-match.png

ETag和Last-Modified的作用和用法也是差不多,说一说他们的区别。
首先在精确度上,Etag要优于Last-Modified。Last-Modified的时间单位是秒,如果某个文件在1秒内改变了多次,那么他们的Last-Modified其实并没有体现出来修改,但是Etag每次都会改变确保了精度;如果是负载均衡的服务器,各个服务器生成的Last-Modified也有可能不一致。

第二在性能上,Etag要逊于Last-Modified,毕竟Last-Modified只需要记录时间,而Etag需要服务器通过算法来计算出一个hash值。
第三在优先级上,服务器校验优先考虑Etag

浏览器缓存过程

  1. 浏览器第一次加载资源,服务器返回200,浏览器将资源文件从服务器上请求下载下来,并把response header及该请求的返回时间一并缓存;

  2. 下一次加载资源时,先比较当前时间和上一次返回200时的时间差,如果没有超过cache-control设置的max-age,则没有过期,命中强缓存,不发请求直接从本地缓存读取该文件(如果浏览器不支持HTTP1.1,则用expires判断是否过期);如果时间过期,则向服务器发送header带有If-None-Match和If-Modified-Since的请求;

  3. 服务器收到请求后,优先根据Etag的值判断被请求的文件有没有做修改,Etag值一致则没有修改,命中协商缓存,返回304;如果不一致则有改动,直接返回新的资源文件带上新的Etag值并返回200;

  4. 如果服务器收到的请求没有Etag值,则将If-Modified-Since和被请求文件的最后修改时间做比对,一致则命中协商缓存,返回304;不一致则返回新的last-modified和文件并返回200;

用户行为对浏览器缓存的控制

  1. 地址栏访问,链接跳转是正常用户行为,将会触发浏览器缓存机制;
  2. F5刷新,浏览器会设置max-age=0,跳过强缓存判断,会进行协商缓存判断;
  3. ctrl+F5刷新,跳过强缓存和协商缓存,直接从服务器拉取资源。

原文链接