一个简单的利用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);
    }
}