SpringSecurity-SpirngBoot-显式声明用户名和使用UserDetailsService(二)
在上一节中,我们实现了SpringSecurity和SpirngBoot的初步整合,没有进行任何配置就实现了一个登录认证的界面,并且在认证成功后可以访问到我们的后端资源。在这一章会介绍到使用InMemoryUserDetailsManager
和UserDetailsService
定制用户。
1.使用InMemoryUserDetailsManager
显式声明用户名
在上一节的基础上,我们使用git创建一个新的分支spring-security-explicit
:
导入相关依赖
导入测试相关依赖,方便单元测试使用
<!--security测试模块-->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
<!-- Selenium Web驱动 -->
<dependency>
<groupId>org.seleniumhq.selenium</groupId>
<artifactId>htmlunit-driver</artifactId>
</dependency>
编写SpringSecurity配置类
@Configurable
@EnableWebSecurity
public class SecurityConfiguration {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
//所有的请求都需要用户进行认证。
.authorizeHttpRequests(
(authorize) -> authorize.anyRequest().authenticated()
)
//开启Basic认证
.httpBasic(Customizer.withDefaults())
//开启表单认证
.formLogin(Customizer.withDefaults());
return http.build();
}
@Bean
public InMemoryUserDetailsManager userDetailService() {
// 显示声明用户名密码
UserDetails user = User.withDefaultPasswordEncoder()
.username("admin")
.password("123456")
.roles("USER")
.build();
return new InMemoryUserDetailsManager(user);
}
}
在userDetailService
方法中可以看到,我们注册了一个InMemoryUserDetailsManager对象到Spring bean容器中,并且向其构造函数传入了我们显式声明的user对象,用户名是admin,密码是123456。
启动项目,访问localhost:8080
第一次访问会跳转到登录页面:
输入我们定义的用户名和密码,点击登录:
可以看到,我们成功访问到了之前定义的index.html页面
单元测试
编写以下单元测试代码,测试在未登录时访问,和使用MockUser登录访问的结果:
@SpringBootTest
@AutoConfigureMockMvc
class JackmouseSpringBootSecurityHelloApplicationTests {
@Test
void contextLoads() {
}
@Autowired
private MockMvc mockMvc;
@Test
void indexWhenUnAuthenticatedThenRedirect() throws Exception {
// @formatter:off
this.mockMvc.perform(get("/"))
.andExpect(status().isUnauthorized());
// @formatter:on
}
@Test
@WithMockUser
void indexWhenAuthenticatedThenOk() throws Exception {
// @formatter:off
this.mockMvc.perform(get("/"))
.andExpect(status().isOk());
// @formatter:on
}
}
2.使用UserDetailsService
定义用户
代码编写
创建CustomUser实体类
public class CustomUser {
private final long id;
private final String email;
@JsonIgnore
private final String password;
@JsonCreator
public CustomUser(long id, String email, String password) {
this.id = id;
this.email = email;
this.password = password;
}
public long getId() {
return this.id;
}
public String getEmail() {
return this.email;
}
public String getPassword() {
return this.password;
}
}
创建CustomUserRepository接口
public interface CustomUserRepository {
CustomUser findCustomUserByEmail(String email);
}
创建MapCustomUserRepository对CustomUserRepository接口实现
public class MapCustomUserRepository implements CustomUserRepository{
private final Map<String, CustomUser> emailToCustomUser;
public MapCustomUserRepository(Map<String, CustomUser> emailToCustomUser) {
this.emailToCustomUser = emailToCustomUser;
}
@Override
public CustomUser findCustomUserByEmail(String email) {
return this.emailToCustomUser.get(email);
}
}
SecurityConfiguration新增代码
- 删除InMemoryUserDetailsManager的bean定义
- 注入BCryptPasswordEncoder密码加密类
- 初始化一个CustomUser供登录使用
@Bean
PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
MapCustomUserRepository userRepository() {
String password = new BCryptPasswordEncoder().encode("123456");
CustomUser customUser = new CustomUser(1L, "1310179240@qq.com", password);
Map<String, CustomUser> emailToCustomUser = new HashMap<>();
emailToCustomUser.put(customUser.getEmail(), customUser);
return new MapCustomUserRepository(emailToCustomUser);
}
创建CustomUserRepositoryUserDetailsService实现UserDetailsService
@Service
public class CustomUserRepositoryUserDetailsService implements UserDetailsService {
private final CustomUserRepository userRepository;
public CustomUserRepositoryUserDetailsService(CustomUserRepository userRepository) {
this.userRepository = userRepository;
}
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 查询用户名对应的用户
CustomUser customUser = this.userRepository.findCustomUserByEmail(username);
if (customUser == null) {
// 用户不存在 抛出异常
throw new UsernameNotFoundException("username " + username + " is not found");
}
return new CustomUserDetails(customUser);
}
static final class CustomUserDetails extends CustomUser implements UserDetails {
private static final List<GrantedAuthority> ROLE_USER = Collections
.unmodifiableList(AuthorityUtils.createAuthorityList("ROLE_USER"));
CustomUserDetails(CustomUser customUser) {
super(customUser.getId(), customUser.getEmail(), customUser.getPassword());
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return ROLE_USER;
}
@Override
public String getUsername() {
return getEmail();
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
}
代码实现了SpringSecurity定义的UserDetailsService
接口,loadUserByUsername
是SpringSecurity在进行登录验证时获取用户信息的模板方法,在我们对loadUserByUsername进行重写后,SpringSecurity会使用我们定义的loadUserByUsername
方法获取用户信息。
userRepository中保存了我们在SecurityConfiguration注入的用户名为1310179240@qq.com,密码为BCryptPasswordEncoder加密后的123456.
定义CurrentUser注解
@AuthenticationPrincipal
@Retention(RetentionPolicy.RUNTIME)
public @interface CurrentUser {
}
@AuthenticationPrincipal
的作用时自动注入当前登录用户信息。
创建UserController类
@RestController
public class UserController {
@GetMapping("/user")
public CustomUser user(@CurrentUser CustomUser currentUser) {
return currentUser;
}
}
这里的@CurrentUser
就会把当前登录的用户信息注入到currentUser中。
启动项目,访问localhost:8080/user
在登陆页面输入用户名和密码,点击登录:
可以看到我们成功访问到了用户的相关信息。
单元测试
UserDetailsServiceTests.java:
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class UserDetailsServiceTests {
@Autowired
private TestRestTemplate rest;
@Test
void login() {
CustomUser result = this.rest.withBasicAuth("1310179240@qq.com", "123456")
.getForObject("/user", CustomUser.class);
assertThat(result.getEmail()).isEqualTo("1310179240@qq.com");
}
}