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);
}
}
以下是我們的自定義Principal “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;
}
}
注意:我們使用了自定義的Principal “UserPrincipal” 而不是 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。
不錯。