英文:
Is it even possible to implement a database backed login process with encryption, using only Spring Security and Data JPA?
问题
根据标题,我试图实现一些东西,目前我甚至不确定我设想的方式是否可行。我想要一个非常简单的、基于数据库的注册和登录流程,以便根据不同的用户显示不同的页面内容。所以它应该按照以下方式工作:
- 用户在注册页面上注册,执行所有检查并创建用户
- 密码使用Bcrypt进行加密,用户名和密码存储在使用Spring Data JPA的数据库中
- 用户通过“标准”Spring Security登录表单登录
- 我自定义的
UserDetailsService
实现获取用户名的数据库条目 - Spring Security比较密码并登录用户
- 成功登录后,我获得一个主体,我可以基于它显示内容
问题在于加密。基本上,我找不到一种实现方式,而不会出现错误。无论我尝试什么,似乎应用程序甚至都不会尝试将登录页面的密码与数据库中的加密密码进行匹配。根据 [Baeldung][1] 的说法,只需定义一个编码器并将其添加到 AuthenticationProvider 中就足够了。然而,当我按照这个教程操作时,我会收到一个错误消息,指出
编码后的密码不像是bcrypt
下面是我的 UserDetailsService
,如注释所述,密码实际上是一个有效的、具有60个字符的Bcrypt密码。
package com.example.demo;
import...
import java.util.Optional;
@Service("myUserDetailsService")
public class MyUserDetailsService implements UserDetailsService {
@Autowired
UserRepository userRepository;
@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
Optional<UserDB> userDB = userRepository.findById(s);
User user = null;
if (userDB.isPresent()) {
System.out.println("present!");
UserDB userDB2 = userDB.get();
System.out.println(userDB2.getPassword());
// 上面的一行打印出一个有效的60个字符的BCrypt密码
user = new User(userDB2.getUsername(), userDB2.getPassword());
}
return user;
}
}
我在这里创建了一个[帖子][2]来寻求帮助,因为我起初不能成功自动装配我的 UserDetailsService
。那里,我得到的唯一答案是以不同于教程的方式定义编码器:
@Bean(name = "passwordEncoder")
@Qualifier("passwordEncoder")
public PasswordEncoder passwordEncoder() {
return PasswordEncoderFactories.createDelegatingPasswordEncoder();
}
这将存储在仓库中的密码从
$2a$10$TEU8lkr/aVzJMgtu78.NceUzy8zJG5FCqHcOgNK61AL5or0McLpTq
更改为
{bcrypt}$2a$10$TEU8lkr/aVzJMgtu78.NceUzy8zJG5FCqHcOgNK61AL5or0McLpTq
但是然后我又遇到了一种新的错误
java.lang.IllegalArgumentException: There is no PasswordEncoder mapped for the id "null"
将我提供的新编码器定义链接给我的用户,他将我链接到[这个问题][3],在那里有人遇到了相同的问题。这就是我提出这个问题的原因。
那里的被接受的答案以及其他一些答案似乎要么使用了 OAuth2(一个答案使用了我甚至无法导入的 ClientDetailsServiceConfigurer
类),要么禁用了加密(或者使用了一个什么都不做的编码器),或者使用了 InMemoryAuthentication 而不是数据库。对我来说,这些都不是有用的,因为我想要真实地使用它并永久地存储我的用户。
在当前版本的 Spring Security 中是否存在阻止此功能工作的问题?在我看来,唯一缺少的步骤是将加密后的密码与登录输入进行比较。之前在使用 inMemoryAuth 和没有加密时,它工作得很好。
或者我是否需要使用额外的服务,比如 OAuth2(我认为它是 Spring Security 的一个附加功能,而不是一个要求)?我只是想知道在我尝试了不同的教程后,哪种方式是有效的,这些教程让它看起来应该非常容易,但事实证明它根本不起作用。
我会向您展示我的代码的所有其他相关部分,也许我在那里犯了一些非常简单的错误:
首先是 WebSecurityConfig
package com.example.demo;
import...
@Configuration
@ComponentScan(basePackages = { "com.example.demo" })
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/", "/main", "/register").permitAll()
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/login")
.permitAll()
.and()
.logout()
.permitAll();
}
@Autowired
private MyUserDetailsService userDetailsService;
@Bean(name = "passwordEncoder")
@Qualifier("passwordEncoder")
public PasswordEncoder passwordEncoder() {
return PasswordEncoderFactories.createDelegatingPasswordEncoder();
}
@Bean
public DaoAuthenticationProvider authProvider() {
DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
authProvider.setUserDetailsService(userDetailsService);
authProvider.setPasswordEncoder(passwordEncoder());
return authProvider;
}
}
注册的 Controller:
@PostMapping("/register")
public String workRegistrationForm(Model model, @ModelAttribute("user") UserDto newUser) {
System.out.println(newUser.getUsername() + ", " + newUser.getEmail());
String encodedPW = encoder.encode(newUser.getPassword());
UserDB newUserDB = new UserDB(newUser.getUsername(), encodedPW);
userRepository.save(newUserDB);
return "redirect:/main";
}
添加登录视图的 MVC Config:
@Configuration
public class MvcConfig implements WebMvcConfigurer {
public void addView
<details>
<summary>英文:</summary>
As the title states, I'm trying to implement something that, at this point, I'm not even sure is possible the way I imagined it. I want a very simple, database backed registration and login process in order to show different page contents to different users. So it should work like this:
- User registers on a regsitration page, all checks are performed and
User is created
- Password is encrypted with Bcrypt and Username and Password are
stored on a database using Spring Data JPA
- User logs in over the "standard" Spring Security login form
- My custom implementation of `UserDetailsService` fetches the database entry for the username
- Spring security compares the passwords and logs in the user
- After successful login, I get a principal and I'm able to display
content based on that
The problem is in the encryption. Basically, I can't find a way to implement it without getting errors. No matter what I tried, it seems like the app does not even try to match the password from the login page with my encrypted password from the database. According to [Baeldung][1], it should be enough to just define an encoder, and add it to the AuthenticationProvider. However, when I follow this tutorial, I get an error that says
> Encoded password does not look like bcrypt
Here is my `UserDetailsService`, as stated in the comment, the password is, in fact, a valid, 60 character Bcrypt.
package com.example.demo;
import...
import java.util.Optional;
@Service("myUserDetailsService")
public class MyUserDetailsService implements UserDetailsService {
@Autowired
UserRepository userRepository;
@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
Optional<UserDB> userDB = userRepository.findById(s);
User user = null;
if (userDB.isPresent()) {
System.out.println("present!");
UserDB userDB2 = userDB.get();
System.out.println(userDB2.getPassword());
// The line above prints a valid BCrypt password with 60 characters
user = new User(userDB2.getUsername(), userDB2.getPassword());
}
return user;
}
}
I created a [post][2] here to ask for help, as the Autowiring of my `UserDetailsService` didn't work at first. There, the only answer I got was to define the Encoder in a different way to the tutorial:
@Bean(name = "passwordEncoder")
@Qualifier("passwordEncoder")
public PasswordEncoder passwordEncoder() {
return PasswordEncoderFactories.createDelegatingPasswordEncoder();
}
This changes the password that is saved to the repository from
$2a$10$TEU8lkr/aVzJMgtu78.NceUzy8zJG5FCqHcOgNK61AL5or0McLpTq
to
{bcrypt}$2a$10$TEU8lkr/aVzJMgtu78.NceUzy8zJG5FCqHcOgNK61AL5or0McLpTq
but then I get a new kind of error
> java.lang.IllegalArgumentException: There is no PasswordEncoder mapped for the id "null"
The user who gave me the new definition of the Encoder linked me to [this question][3], where someone had the same problem. This is what lead me to phrase this question the way I did.
The accepted answer there, as well as a few others, seem to either use Oauth2 (one answer uses the class `ClientDetailsServiceConfigurer` that I can't even import), disable encryption (or rather use an Encoder that does nothing) or use InMemoryAuthentication instead of a database. None of this is useful to me, as I want to use it for real and store my users permanently.
Is there some kind of problem in the current version of Spring Security that prevents this from working? It seems to me that the only step that's missing is the comparison of the encrypted password and the login input. It worked fine before with inMemoryAuth and no encryption.
Or do I have to use some additional service like OAuth2 ( I thought it was an addition to Spring Security, not a requirement)? I just want to know what works before I spend another three days trying out different tutorials that make it seem like it should work really easy, and then it turns out it somehow doesn't work at all.
I'll show you all the other relevant parts of my code, maybe I just made some super simple mistake there:
First the **WebSecurityConfig**
package com.example.demo;
import...
@Configuration
@ComponentScan(basePackages = { "com.example.demo" })
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/", "/main", "/register").permitAll()
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/login")
.permitAll()
.and()
.logout()
.permitAll();
}
@Autowired
private MyUserDetailsService userDetailsService;
@Bean(name = "passwordEncoder")
@Qualifier("passwordEncoder")
public PasswordEncoder passwordEncoder() {
return PasswordEncoderFactories.createDelegatingPasswordEncoder();
}
@Bean
public DaoAuthenticationProvider authProvider() {
DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
authProvider.setUserDetailsService(userDetailsService);
authProvider.setPasswordEncoder(passwordEncoder());
return authProvider;
}
}
The **Controller** for the registration:
@PostMapping("/register")
public String workRegistrationForm(Model model, @ModelAttribute("user") UserDto newUser) {
System.out.println(newUser.getUsername() + ", " + newUser.getEmail());
String encodedPW = encoder.encode(newUser.getPassword());
UserDB newUserDB = new UserDB(newUser.getUsername(), encodedPW);
userRepository.save(newUserDB);
return "redirect:/main";
}
**MVC Config** that adds the login view:
@Configuration
public class MvcConfig implements WebMvcConfigurer {
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/home").setViewName("home");
registry.addViewController("/").setViewName("home");
registry.addViewController("/securedtwo").setViewName("securedtwo");
registry.addViewController("/login").setViewName("login");
}
}
The login.html:
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="https://www.thymeleaf.org"
xmlns:sec="https://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
<head>
<title>Spring Security Example </title>
</head>
<body>
<div th:if="${param.error}">
Invalid username and password.
</div>
<div th:if="${param.logout}">
You have been logged out.
</div>
<form th:action="@{/login}" method="post">
<div><label> User Name : <input type="text" name="username"/> </label></div>
<div><label> Password: <input type="password" name="password"/> </label></div>
<div><input type="submit" value="Sign In"/></div>
</form>
</body>
</html>
The **User** that implements UserDetails (UserDB is basically the same without the authorities):
public class User implements UserDetails {
String password;
String username;
public User(String password, String username) {
this.password = password;
this.username = username;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
ArrayList<GrantedAuthority> rights = new ArrayList<>();
rights.add(new SimpleGrantedAuthority("USER"));
return rights;
}
..Getters and Setters
Please help me. Anything you can tell me is appreciated. It seemed like this should be super simple, but so far it's been nothing but frustrating for me. I really can't tell what it is I might be doing wrong, and the worst is it feels like I'm really close.
Another option I thought of: In case this can't be made to work, could I simply do the check myself? What class and method would I have to override to be able to do the simple task of comparing the passwords with my own logic and just tell Spring Security "This user is valid"?
Thanks in advance!
[1]: https://www.baeldung.com/spring-security-registration-password-encoding-bcrypt
[2]: https://stackoverflow.com/questions/63824974/spring-security-cant-autowire-my-userdetailservice-to-authenticationprovider-f
[3]: https://stackoverflow.com/questions/49654143/spring-security-5-there-is-no-passwordencoder-mapped-for-the-id-null
</details>
# 答案1
**得分**: 1
以下是您要求的翻译内容:
看起来您的数据库中有一些未标记为 `bcrypt` 的密码。请尝试:
```java
public PasswordEncoder passwordEncoder() {
DelegatingPasswordEncoder encoder =
PasswordEncoderFactories.createDelegatingPasswordEncoder();
encoder.setDefaultPasswordEncoderForMatches(new BCryptPasswordEncoder());
return encoder;
}
这样可以将任何没有“{id}”的密码解释为 bcrypt。
[已更新。]
另一个变更是从我的代码中推出的,需要在您的 WebSecurityConfigurerAdapter
实现中添加一个方法:
public class WebSecurityConfigurerImpl extends WebSecurityConfigurerAdapter {
...
@Autowired private UserDetailsService userDetailsService;
@Autowired private PasswordEncoder passwordEncoder;
@Override
public void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService)
.passwordEncoder(passwordEncoder);
}
...
}
希望这对您有所帮助。
英文:
It looks like you have bcrypt
passwords in your database that are not labeled as bcrypt
. Try:
public PasswordEncoder passwordEncoder() {
DelegatingPasswordEncoder encoder =
PasswordEncoderFactories.createDelegatingPasswordEncoder();
encoder.setDefaultPasswordEncoderForMatches(new BCryptPasswordEncoder());
return encoder;
}
This should interpret any password without an "{id}" as bcrypt.
[Updated.]
One other change driven from. my code is to add a method to your WebSecurityConfigurerAdapter
implementation:
public class WebSecurityConfigurerImpl extends WebSecurityConfigurerAdapter {
...
@Autowired private UserDetailsService userDetailsService;
@Autowired private PasswordEncoder passwordEncoder;
@Override
public void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService)
.passwordEncoder(passwordEncoder);
}
...
}
Hopefully, this helps.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论