從Reddit App中解耦註冊與登錄

REST,Spring
Remote
1
06:15 PM · Dec 01 ,2025

1. 概述

在本教程中 – 我們將用一種更簡單的基於表單的登錄流程,替換掉由Reddit支持的OAuth2身份驗證流程

我們仍然可以登錄後將Reddit連接到應用程序,但不再使用Reddit來驅動我們的主要登錄流程。

2. Basic User Registration

首先,讓我們替換舊的身份驗證流程。

2.1. User 實體

我們將對 User 實體進行一些更改:使 username 唯一,添加 password 字段(臨時):

@Entity
public class User {
    ...

    @Column(nullable = false, unique = true)
    private String username;

    private String password;

    ...
}

2.2. 註冊新的用户

接下來,讓我們在後端註冊新的用户:

@Controller
@RequestMapping(value = "/user")
public class UserController {

    @Autowired
    private UserService service;

    @RequestMapping(value = "/register", method = RequestMethod.POST)
    @ResponseStatus(HttpStatus.OK)
    public void register(
      @RequestParam("username") String username, 
      @RequestParam("email") String email,
      @RequestParam("password") String password) 
    {
        service.registerNewUser(username, email, password);
    }
}

顯然這是一個基本的創建用户操作——沒有花哨的功能。

以下是 實際的實現,在服務層

@Service
public class UserService {
    @Autowired
    private UserRepository userRepository;

    @Autowired
    private PreferenceRepository preferenceReopsitory;

    @Autowired
    private PasswordEncoder passwordEncoder;

    @Override
    public void registerNewUser(String username, String email, String password) {
        User existingUser = userRepository.findByUsername(username);
        if (existingUser != null) {
            throw new UsernameAlreadyExistsException("Username already exists");
        }
        
        User user = new User();
        user.setUsername(username);
        user.setPassword(passwordEncoder.encode(password));
        Preference pref = new Preference();
        pref.setTimezone(TimeZone.getDefault().getID());
        pref.setEmail(email);
        preferenceReopsitory.save(pref);
        user.setPreference(pref);
        userRepository.save(user);
    }
}

2.3. 處理異常

以及簡單的 UserAlreadyExistsException

public class UsernameAlreadyExistsException extends RuntimeException {

    public UsernameAlreadyExistsException(String message) {
        super(message);
    }
    public UsernameAlreadyExistsException(String message, Throwable cause) {
        super(message, cause);
    }
}

異常處理 在應用程序的主異常處理程序中

@ExceptionHandler({ UsernameAlreadyExistsException.class })
public ResponseEntity<Object> 
  handleUsernameAlreadyExists(RuntimeException ex, WebRequest request) {
    logger.error("400 Status Code", ex);
    String bodyOfResponse = ex.getLocalizedMessage();
    return new 
      ResponseEntity<Object>(bodyOfResponse, new HttpHeaders(), HttpStatus.BAD_REQUEST);
}

2.4. 一個簡單的註冊頁面

最後,一個簡單的前端 signup.html

<form>
    <input  id="username"/>
    <input  id="email"/>
    <input type="password" id="password" />
    <button onclick="register()">Sign up</button>
</form>

<script>
function register(){
    $.post("user/register", {username: $("#username").val(),
      email: $("#email").val(), password: $("#password").val()}, 
      function (data){
        window.location.href= "./";
    }).fail(function(error){
        alert("Error: "+ error.responseText);
    }); 
}
</script>

再次提到,這並不是一個成熟的註冊流程——只是一個非常快的流程。對於完整的註冊流程,您可以查看 Baeldung 上的主要註冊系列。

3. 新登錄頁面以下是我們的新且簡單的登錄頁面:

<div th:if="${param.containsKey('error')}">
Invalid username or password
</div>
<form method="post" action="j_spring_security_check">
    <input name="username" />
    <input type="password" name="password"/>  
    <button type="submit" >Login</button>
</form>
<a href="signup">Sign up</a>

4. 安全配置現在,讓我們來查看新的安全配置:

@Configuration
@EnableWebSecurity
@ComponentScan({ "org.baeldung.security" })
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private MyUserDetailsService userDetailsService;

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService).passwordEncoder(encoder());
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            ...
            .formLogin()
            .loginPage("/")
            .loginProcessingUrl("/j_spring_security_check")
            .defaultSuccessUrl("/home")
            .failureUrl("/?error=true")
            .usernameParameter("username")
            .passwordParameter("password")
            ...
    }

    @Bean
    public PasswordEncoder encoder() { 
        return new BCryptPasswordEncoder(11); 
    }
}

大多數情況都比較簡單,所以我們不會進行詳細的説明。

以下是我們的自定義UserDetailsService:

@Service
public class MyUserDetailsService implements UserDetailsService {

    @Autowired
    private UserRepository userRepository;

    @Override
    public UserDetails loadUserByUsername(String username) {
        User user = userRepository.findByUsername(username); 
        if (user == null) { 
            throw new UsernameNotFoundException(username);
        } 
        return new UserPrincipal(user);
    }
}

以下是我們的自定義PrincipalUserPrincipal” 實現了UserDetails:

public class UserPrincipal implements UserDetails {

    private User user;

    public UserPrincipal(User user) {
        super();
        this.user = user;
    }

    @Override
    public String getUsername() {
        return user.getUsername();
    }

    @Override
    public String getPassword() {
        return user.getPassword();
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return Arrays.asList(new SimpleGrantedAuthority("ROLE_USER"));
    }

    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return true;
    }
}

注意:我們使用了自定義的PrincipalUserPrincipal” 而不是 Spring Security 默認的 User

5. 認證 Reddit

現在我們不再依賴 Reddit 進行認證流程,因此我們需要允許用户在登錄後將他們的賬户連接到 Reddit

首先,我們需要修改舊的 Reddit 登錄邏輯:

@RequestMapping("/redditLogin")
public String redditLogin() {
    OAuth2AccessToken token = redditTemplate.getAccessToken();
    service.connectReddit(redditTemplate.needsCaptcha(), token);
    return "redirect:home";
}

以及實際的實現——connectReddit()方法:

@Override
public void connectReddit(boolean needsCaptcha, OAuth2AccessToken token) {
    UserPrincipal userPrincipal = (UserPrincipal) 
      SecurityContextHolder.getContext().getAuthentication().getPrincipal();
    User currentUser = userPrincipal.getUser();
    currentUser.setNeedCaptcha(needsCaptcha);
    currentUser.setAccessToken(token.getValue());
    currentUser.setRefreshToken(token.getRefreshToken().getValue());
    currentUser.setTokenExpiration(token.getExpiration());
    userRepository.save(currentUser);
}

請注意,redditLogin()邏輯現在用於將用户的賬户連接到我們的系統與他的 Reddit 賬户,通過獲取用户的AccessToken

至於前端,那相當簡單:

<h1>歡迎, 
<a href="profile" sec:authentication="principal.username">Bob</a></small>
</h1>
<a th:if="${#authentication.principal.user.accessToken == null}" href="redditLogin" >
    連接你的賬户到 Reddit
</a>

我們還需要確保用户在嘗試提交帖子之前連接他們的賬户到 Reddit:

@RequestMapping("/post")
public String showSubmissionForm(Model model) {
    if (getCurrentUser().getAccessToken() == null) {
        model.addAttribute("msg", "抱歉,您尚未連接您的賬户到 Reddit");
        return "submissionResponse";
    }
    ...
}

6. 結論

小紅書應用正在穩步發展。

舊的認證流程——由Reddit完全支持——存在一些問題。現在,我們擁有一個乾淨簡潔的基於表單的登錄方式,同時仍然可以連接後端Reddit API。

不錯。

user avatar
0 位用戶收藏了這個故事!
收藏

發佈 評論

Some HTML is okay.