Перейти к основному содержимому

Отделение регистрации от входа в приложение Reddit

· 5 мин. чтения

1. Обзор

В этом руководстве мы заменим процесс аутентификации OAuth2, поддерживаемый Reddit, более простым входом в систему на основе формы .

Мы по-прежнему сможем подключить Reddit к приложению после входа в систему, мы просто не будем использовать Reddit для управления нашим основным потоком входа.

2. Базовая регистрация пользователя

Во-первых, давайте заменим старый поток аутентификации.

2.1. Пользовательская сущность _

Мы внесем несколько изменений в сущность User: сделаем имя пользователя уникальным, добавим поле пароля (временное):

@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>

Стоит еще раз отметить, что это не полностью зрелый процесс регистрации — просто очень быстрый процесс. Для полного процесса регистрации вы можете ознакомиться с основной регистрационной серией здесь, на ForEach.

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.foreach.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);
}
}

А вот наш пользовательский Принципал « UserPrincipal» , который реализует 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;
}
}

Примечание. Мы использовали нашего пользовательского принципала « UserPrincipal» вместо пользователя Spring Security по умолчанию .

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>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>

Нам также необходимо убедиться, что пользователи подключают свои учетные записи к 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. Заключение

Небольшое приложение Reddit определенно движется вперед.

Старый процесс аутентификации, полностью поддерживаемый Reddit, вызывал некоторые проблемы. Итак, теперь у нас есть чистый и простой вход в систему на основе формы, но при этом мы все еще можем подключить ваш Reddit API в серверной части.

Хорошая вещь.