一个简单的利用Spring Security实现用户登陆(用户认证)的例子。 使用MongoDB作为数据库来存储用户名和密码。
Spring Security的用户认证默认没有集成MongoDB。
可以通过实现AuthenticationProvider
接口来完成个性化的用户认证。
@Component
public class AuthenticationProviderImpl implements AuthenticationProvider{
@Autowired
private UserRepository repository;
@Autowired
private BCryptPasswordEncoder passwordEncoder;
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
String username = authentication.getName();
String password = authentication.getCredentials().toString();
List<User> users = repository.findByAccount(username);
if (users.isEmpty()) {
throw new UsernameNotFoundException("user not found");
}
User user = users.get(0);
if (!passwordEncoder.matches(password, user.getEncodedPassword())) {
throw new BadCredentialsException("user name or password is invalid");
}
// NOTE: should provide @authorities param, otherwise the returned authentication's authorities will not
// match the configuration in WebSecurityConfigurerAdapter. And it causes "defaultSuccessUrl" unaccessible
// due to authorities mismatch.
// 注意第三个参数,需要和WebSecurityConfigurerAdapter配置的role一致
UsernamePasswordAuthenticationToken result = new UsernamePasswordAuthenticationToken(
authentication.getName(), authentication.getCredentials(), Roles.USER_AUTHORITIES);
return result;
}
@Override
public boolean supports(Class<?> authentication) {
// 表示这个AuthenticationProvider支持简单的用户名密码登陆
return UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication);
}
}
UserRepository
是利用Spring Data MongoDB来实现的一个DAO。
public interface UserRepository extends MongoRepository<User, ObjectId>{
List<User> findByAccount(String account);
}
密码使用BCryptPasswordEncoder来加密,更安全。
Spring Security的配置如下。
简单起见用了一个静态页面,login.html
,来作为登陆页面。
/login
是一个POST请求,Spring Security默认要求POST请求带一个CSRF token。
作为测试,可以关闭CSRF保护,也可以暴露CSRF token的GET接口然后在静态页面里用Javascript得到这个token。
详见Spring Security文档。
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/csrf").permitAll()
.antMatchers("/signup.html").permitAll()
.antMatchers("/users/signup").permitAll()
// if no ".antMatchers("/js/**").permitAll()", when request javascript files
// the server will not return "Content-Type" header in the response.
// and chrome will raise following error when it tries to execute javascript files.
// "Refused to execute script from 'http://localhost:8801/js/jquery-3.1.1.js'
// because its MIME type ('text/html') is not executable, and strict MIME type checking is enabled."
// TODO why missing following line will cause no "Content-Type" header?
.antMatchers("/js/**").permitAll()
.antMatchers("/**").hasRole(Roles.USER)
.and()
.formLogin()
.usernameParameter("username")
.passwordParameter("password")
.loginPage("/login.html").permitAll() // allow any body to access login page
.loginProcessingUrl("/login")
.defaultSuccessUrl("/home.html")
.failureUrl("/login-error.html");
}
@Bean
public BCryptPasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
简单的登陆页面,
<html>
<head>
<script src="js/jquery-3.1.1.js"></script>
</head>
<body>
<form action="/login" method="post">
<div>
<label>user name</label>
<input type="text" placeholder="enter username" name="username" required>
<label>password</label>
<input type="password" placeholder="enter password" name="password" required>
<input type="hidden" name="_csrf" value="">
<button type="submit"> login</button>
</div>
</form>
<script>
$(function(){
$.get("/csrf", function(data){
$('input[name="_csrf"]').attr('value', data.token);
});
});
</script>
</body>
</html>
简单的用户注册,
@Controller
@RequestMapping("/users")
public class UserController {
@Autowired
private UserRepository repository;
@Autowired
private BCryptPasswordEncoder passwordEncoder;
@PostMapping("/signup")
@ResponseBody
public ResponseEntity<Void> signup(@RequestBody MultiValueMap<String, String> params) {
String account = params.getFirst("username");
String password = params.getFirst("password");
String confirmed = params.getFirst("confirmed");
// ... 检查是否已经注册、非空、二次输入正确、密码强度等
User user = new User();
user.setAccount(account);
user.setEncodedPassword(passwordEncoder.encode(password));
repository.insert(user);
return new ResponseEntity<Void>(HttpStatus.CREATED);
}
}