Spring Security小试牛刀

前言

前一篇介绍了 Spring Security 入门的基础准备。从今天开始我们来一步步窥探它是如何工作的。

准备

Spring Boot集成 Spring Security 只需要引入其对应的依赖即可

1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>

添加完成后声明一个index路由:

1
2
3
4
5
6
7
8
@Controller
public class MainController {

@RequestMapping(value = {"", "/", "/index"})
public String main() {
return "index";
}
}

这里我返回了一个首页index,完成后启动项目,访问:http://127.0.0.1:8080会自动跳转到一个登陆页面http://127.0.0.1:8080/login如下:
登陆页面

这里是因为我们项目中引入了Spring Security以后,自动装配了Spring Security的环境,Spring Security的默认配置是要求经过认证成功后才可以访问到URL对应的资源,Spring Security的默认用户名是:user密码则是一串uuid字符串,输出在控制台里,仔细看一下下图,你可以发现该随机密码是由UserDetailsServiceAutoConfiguration配置生成的:
控制台输出

UserDetailsServiceAutoConfiguration

UserDetailsServiceAutoConfiguration全包名为: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
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
/*
* Copyright 2012-2019 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.springframework.boot.autoconfigure.security.servlet;

import java.util.List;
import java.util.regex.Pattern;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import org.springframework.beans.factory.ObjectProvider;
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.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.security.SecurityProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Lazy;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.config.annotation.ObjectPostProcessor;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.util.StringUtils;

/**
* {@link EnableAutoConfiguration Auto-configuration} for a Spring Security in-memory
* {@link AuthenticationManager}. Adds an {@link InMemoryUserDetailsManager} with a
* default user and generated password. This can be disabled by providing a bean of type
* {@link AuthenticationManager}, {@link AuthenticationProvider} or
* {@link UserDetailsService}.
*
* @author Dave Syer
* @author Rob Winch
* @author Madhura Bhave
* @since 2.0.0
*/
@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;
}

}

关于几个注解的解释:

  • @ConditionalOnBean 当给定的在bean存在时,则实例化当前Bean
  • @ConditionalOnMissingBean 当给定的在bean不存在时,则实例化当前Bean
  • @ConditionalOnClass 当给定的类名在类路径上存在,则实例化当前Bean
  • @ConditionalOnMissingClass 当给定的类名在类路径上不存在,则实例化当前Bean

从几个@Conditional注解我们可以看出UserDetailsServiceAutoConfiguration这个类在类路径下存在AuthenticationManager、在Spring容器中存在BeanObjectPostProcessor并且不存在Bean AuthenticationManager, AuthenticationProvider, UserDetailsService的情况下才会生效;
该类初始化了一个名为inMemoryUserDetailsManager的基于内存的用户管理器,并且调用getOrDeducePassword()为我们生成了一个随机密码,SecurityProperties.User user = properties.getUser();这就是我们上面项目启动时,默认加载的用户:user

InMemoryUserDetailsManager

可以看出上面的UserDetailsServiceAutoConfiguration中初始化了一个名为inMemoryUserDetailsManager的类,全包名为:org.springframework.security.provisioning.InMemoryUserDetailsManager,其源码为:

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
/*
* Copyright 2002-2016 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.provisioning;

import java.util.Collection;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsPasswordService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.core.userdetails.memory.UserAttribute;
import org.springframework.security.core.userdetails.memory.UserAttributeEditor;
import org.springframework.util.Assert;

/**
* Non-persistent implementation of {@code UserDetailsManager} which is backed by an
* in-memory map.
* <p>
* Mainly intended for testing and demonstration purposes, where a full blown persistent
* system isn't required.
*
* @author Luke Taylor
* @since 3.1
*/
public class InMemoryUserDetailsManager implements UserDetailsManager,
UserDetailsPasswordService {
protected final Log logger = LogFactory.getLog(getClass());

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

private AuthenticationManager authenticationManager;

public InMemoryUserDetailsManager() {
}

public InMemoryUserDetailsManager(Collection<UserDetails> users) {
for (UserDetails user : users) {
createUser(user);
}
}

public InMemoryUserDetailsManager(UserDetails... users) {
for (UserDetails user : users) {
createUser(user);
}
}

public InMemoryUserDetailsManager(Properties users) {
Enumeration<?> names = users.propertyNames();
UserAttributeEditor editor = new UserAttributeEditor();

while (names.hasMoreElements()) {
String name = (String) names.nextElement();
editor.setAsText(users.getProperty(name));
UserAttribute attr = (UserAttribute) editor.getValue();
UserDetails user = new User(name, attr.getPassword(), attr.isEnabled(), true,
true, true, attr.getAuthorities());
createUser(user);
}
}

public void createUser(UserDetails user) {
Assert.isTrue(!userExists(user.getUsername()), "user should not exist");

users.put(user.getUsername().toLowerCase(), new MutableUser(user));
}

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

public void updateUser(UserDetails user) {
Assert.isTrue(userExists(user.getUsername()), "user should exist");

users.put(user.getUsername().toLowerCase(), new MutableUser(user));
}

public boolean userExists(String username) {
return users.containsKey(username.toLowerCase());
}

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();

logger.debug("Changing password for user '" + username + "'");

// If an authentication manager has been set, re-authenticate the user with the
// supplied password.
if (authenticationManager != null) {
logger.debug("Reauthenticating user '" + username
+ "' for password change request.");

authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(
username, oldPassword));
}
else {
logger.debug("No authentication manager set. Password won't be re-checked.");
}

MutableUserDetails user = users.get(username);

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

user.setPassword(newPassword);
}

@Override
public UserDetails updatePassword(UserDetails user, String newPassword) {
String username = user.getUsername();
MutableUserDetails mutableUser = this.users.get(username.toLowerCase());
mutableUser.setPassword(newPassword);
return mutableUser;
}

public UserDetails loadUserByUsername(String username)
throws UsernameNotFoundException {
UserDetails user = users.get(username.toLowerCase());

if (user == null) {
throw new UsernameNotFoundException(username);
}

return new User(user.getUsername(), user.getPassword(), user.isEnabled(),
user.isAccountNonExpired(), user.isCredentialsNonExpired(),
user.isAccountNonLocked(), user.getAuthorities());
}

public void setAuthenticationManager(AuthenticationManager authenticationManager) {
this.authenticationManager = authenticationManager;
}
}

从该类中我们可以看出,其中的几个方法,其作用就是对用户(UserDetails)的增删改查:

  • createUser(UserDetails user)创建UserDetails
  • updateUser(UserDetails user)更新UserDetails
  • deleteUser(String username)根据username删除UserDetails
  • userExists(String username)根据username检查系统中是否存在对应的UserDetails
  • changePassword(String oldPassword, String newPassword)修改密码
  • loadUserByUsername(String username)通过用户名获得对应的UserDetails

该类还实现了UserDetailsManagerUserDetailsPasswordService接口,而UserDetailsManager类又继承了UserDetailsService接口,这几个类的关系如下图:
UserDetailsServiceAutoConfiguration结构

总结

本章简单的对Spring Security进行了一些解读,其中简单介绍了几个相关的类,相信你已经对在Spring Security中如何加载用户信息有所掌握了,后面我们会由浅入深慢慢解读Spring Security。