Decoupling Registration from Login in the Reddit App – Reddit应用程序中的注册与登录脱钩

最后修改: 2015年 7月 10日

中文/混合/英文(键盘快捷键:t)

1. Overview

1.概述

In this tutorial – we’ll replace the Reddit backed OAuth2 authentication process with a simpler, form-based login.

在本教程中–我们将用一个更简单的、基于表单的登录方式取代Reddit支持的OAuth2认证过程

We’ll still be able to hook Reddit up to the application after we log in, we’ll just not use Reddit to drive our main login flow.

我们仍然能够在登录后将Reddit与应用程序挂钩,我们只是不使用Reddit来驱动我们的主要登录流程。

2. Basic User Registration

2.基本用户注册

First, let’s replace the old authentication flow.

首先,让我们替换旧的认证流程。

2.1. The User Entity

2.1.用户实体

We’ll make a few changes to the User entity: make the username unique, add a password field (temporary) :

我们将对用户实体做一些修改:使用户名唯一,添加一个密码字段(临时)。

@Entity
public class User {
    ...

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

    private String password;

    ...
}

2.2. Register a New User

2.2.注册一个新用户

Next – let’s see how to register a new user in the backend:

接下来 – 让我们看看如何在后台注册一个新用户。

@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);
    }
}

Obviously this is a basic create operation for the user – no bells and whistles.

很明显,这对用户来说是一个基本的创建操作–没有任何花哨的东西。

Here’s the actual implementation, in the service layer:

这里是实际的实现,在服务层

@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. Dealing With Exceptions

2.3.处理异常情况

And the simple UserAlreadyExistsException:

还有简单的UserAlreadyExistsException

public class UsernameAlreadyExistsException extends RuntimeException {

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

The exception is dealt with in the main exception handler of the application:

异常是在应用程序的主异常处理程序中处理的

@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. A Simple Register Page

2.4.一个简单的注册页面

Finally – a simple front-end signup.html:

最后–一个简单的前端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>

It’s worth mentioning again that this isn’t a fully mature registration process – just a very quick flow. For a complete registration flow, you can check out the main registration series here on Baeldung.

值得再次提及的是,这并不是一个完全成熟的注册流程–只是一个非常快速的流程。关于完整的注册流程,你可以查看Baeldung这里的主要注册系列

3. New Login Page

3.新的登录页面

Here is our new and simple login page:

这里是我们的新的和简单的登录页面

<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. Security Configuration

4.安全配置

Now – let’s take a look at the new security configuration:

现在–让我们看一下新的安全配置

@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); 
    }
}

Most things are pretty straightforward, so we won’t go over them in detail here.

大多数事情都很简单,所以我们在此不作详细介绍。

And here’s the custom UserDetailsService:

这里是自定义的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);
    }
}

And here is our custom PrincipalUserPrincipal” that implements UserDetails:

这里是我们自定义的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;
    }
}

Note: We used our custom PrincipalUserPrincipal” instead of Spring Security default User.

注意:我们使用了我们自定义的PrincipalUserPrincipal”而不是Spring Security默认的User

5. Authenticate Reddit

5.认证Reddit</strong

Now that we’re no longer relying on Reddit for our authentication flow, we need to enable users to connect their accounts to Reddit after they log in.

现在我们的认证流程不再依赖Reddit,我们需要让用户在登录后将其账户连接到Reddit

First – we need to modify the old Reddit login logic:

首先–我们需要修改旧的Reddit登录逻辑。

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

And the actual implementation – the connectReddit() method:

而实际的实现–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);
}

Note how the redditLogin() logic is now used to connect the user’s account in our system with his Reddit account by obtaining the user’s AccessToken.

请注意redditLogin()逻辑现在是如何通过获得用户的AccessToken来连接用户在我们系统中的账户和他的Reddit账户。

As for the frontend – that’s quite simple:

至于前端–这很简单。

<h1>Welcome, 
<a href="profile" sec:authentication="principal.username">Bob</a></small>
</h1>
<a th:if="${#authentication.principal.user.accessToken == null}" href="redditLogin" >
    Connect your Account to Reddit
</a>

We need to also need to make sure that users do connect their accounts to Reddit before trying to submit posts:

我们还需要确保用户在尝试提交帖子之前确实将其账户连接到Reddit。

@RequestMapping("/post")
public String showSubmissionForm(Model model) {
    if (getCurrentUser().getAccessToken() == null) {
        model.addAttribute("msg", "Sorry, You did not connect your account to Reddit yet");
        return "submissionResponse";
    }
    ...
}

6. Conclusion

6.结论

The small reddit app is definitely moving forward.

小小的reddit应用肯定在向前发展。

The old authentication flow – fully backed by Reddit – was causing some problems. So now, we have a clean and simple form-based login while still being able to connect your Reddit API in the back end.

旧的认证流程–完全由Reddit支持–造成了一些问题。因此,现在,我们有一个干净和简单的基于表单的登录,同时仍然能够在后端连接你的Reddit API。

Good stuff.

好东西。