前言

在 Spring Boot + Vue 前后端分离项目中,后端只提供接口,页面处理和跳转都由前端实现,前后端通过 json 传输数据。

后端项目,搭建骨架,可以参考文章:使用 MybatisGenerator 根据数据库自动生成 model、mapper 接口和 mapper.xml

接下来开始后端登录接口的实现。

处理 User 用户类

让 User 类实现接口 UserDetails,并重写其中的方法:

public class User implements Serializable, UserDetails {

    ......

    @Override
    public boolean isAccountNonExpired() {
        return true; // 账户没有过期
    }

    @Override
    public boolean isAccountNonLocked() {
        return true; // 账户没有锁定
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true; // 密码没有过期
    }

    @Override
    public boolean isEnabled() {
        return enabled;  // 账户可以使用(需要删除 User 类自带的 getEnabled 方法,如果有的话)
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return null;  // 暂不设置
    }

    ......
}

这里的 User 类,就是整个项目存放用户信息的类,有用户名、密码之类的。

UserService 配置

在包 service 下,新建一个 UserService 类,实现接口 UserDetailsService:

@Service
public class UserService implements UserDetailsService {

    @Autowired
    UserMapper userMapper;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        User user = userMapper.loadUserByUsername(username);
        if (user == null){
            throw new UsernameNotFoundException("用户名不存在!");
        }
        return user;
    }
}

关键代码:

User user = userMapper.loadUserByUsername(username);

这里重写了 loadUserByUsername 方法,通过用户名加载用户信息。

UserMapper 配置

在接口 UserMapper 中,定义 loadUserByUsername 方法:

public interface UserMapper {

    ......
    User loadUserByUsername(String username);
}

UserMapper.xml 配置

在 UserMapper.xml 实现方法 loadUserByUsername ,只需要添加一个查询语句:

......
  <select id="loadUserByUsername" resultMap="BaseResultMap">
    select * from user where username=#{username};
  </select>
  ......

编写 RespBean 类

RespBean 类主要用于向前端返回数据,会在后端很多地方用到,比如 SpringSecurity 配置中就会用到:

public class RespBean {
    private Integer status;
    private String msg;
    private Object obj;

    public static RespBean ok(String msg){
        return new RespBean(200, msg, null);
    }

    public static RespBean ok(String msg, Object obj){
        return new RespBean(200, msg, obj);
    }

    public static RespBean error(String msg){
        return new RespBean(500, msg, null);
    }

    public static RespBean error(String msg, Object obj){
        return new RespBean(500, msg, obj);
    }

    private RespBean() {
    }

    private RespBean(Integer status, String msg, Object obj) {
        this.status = status;
        this.msg = msg;
        this.obj = obj;
    }

    public Integer getStatus() {
        return status;
    }

    public void setStatus(Integer status) {
        this.status = status;
    }

    public String getMsg() {
        return msg;
    }

    public void setMsg(String msg) {
        this.msg = msg;
    }

    public Object getObj() {
        return obj;
    }

    public void setObj(Object obj) {
        this.obj = obj;
    }
}

注意一下,RespBean 类有两个构造函数,都是 private 修饰,表明其他的类就不能直接调用 RespBean 生成新的对象,这样,RespBean 类只有一个对象实例。

这里定义了两个静态方法 ok 和 error ,返回值都是 RespBean 对象。

Spring Security 配置

一般在 Spring Boot + Vue 前后端分离项目中,都是采用 Spring Security 作访问和权限控制。

关于 Spring Security 的权限控制暂且不谈,这里主要是登录接口的配置。

新建一个包 config,再建一个配置类 SecurityConfig ,继承 WebSecurityConfigurerAdapter :

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    UserService userService;

//    如果密码采用 BCryptPasswordEncoder 加密,则取消注释
//    @Bean
//    PasswordEncoder passwordEncoder(){
//        return new BCryptPasswordEncoder();
//    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userService);
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests().anyRequest().authenticated()
                .and()
                .formLogin()
                .usernameParameter("username")
                .passwordParameter("password")
                .loginProcessingUrl("/doLogin")
                .loginPage("/login")
                .successHandler(new AuthenticationSuccessHandler() {
                    @Override
                    public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
                        httpServletResponse.setContentType("application/json;charset=utf-8");
                        PrintWriter writer = httpServletResponse.getWriter();
                        User user = (User) authentication.getPrincipal();
                        user.setPassword(null);
                        RespBean ok = RespBean.ok("登录成功!", user);
                        String string = new ObjectMapper().writeValueAsString(ok);
                        writer.write(string);
                        writer.flush();
                        writer.close();
                    }
                })
                .failureHandler(new AuthenticationFailureHandler() {
                    @Override
                    public void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
                        httpServletResponse.setContentType("application/json;charset=utf-8");
                        PrintWriter writer = httpServletResponse.getWriter();
                        RespBean respBean = RespBean.error("登录失败!");
                        if (httpServletResponse instanceof LockedException){
                            respBean.setMsg("账户被锁定,请联系管理员!");
                        }else if (httpServletResponse instanceof BadCredentialsException){
                            respBean.setMsg("用户名或密码输入错误!");
                        }else if (httpServletResponse instanceof DisabledException){
                            respBean.setMsg("账户被禁用,请联系管理员!");
                        }else if (httpServletResponse instanceof AccountExpiredException){
                            respBean.setMsg("账户过期,请联系管理员!");
                        }else if (httpServletResponse instanceof CredentialsExpiredException){
                            respBean.setMsg("密码过期,请联系管理员!");
                        }else {
                            respBean.setMsg("登录失败!");
                        }
                        String string = new ObjectMapper().writeValueAsString(respBean);
                        writer.write(string);
                        writer.flush();
                        writer.close();
                    }
                })
                .permitAll()
                .and()
                .logout()
                .logoutSuccessHandler(new LogoutSuccessHandler() {
                    @Override
                    public void onLogoutSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
                        httpServletResponse.setContentType("application/json;charset=utf-8");
                        PrintWriter writer = httpServletResponse.getWriter();
                        writer.write(new ObjectMapper().writeValueAsString(RespBean.ok("注销登录")));
                        writer.flush();
                        writer.close();
                    }
                })
                .permitAll()
                .and()
                .csrf().disable();
    }
}

这个配置类比较长,我们挨个分析。

由于我这个数据库中 User 类的密码采用的是 md5DigestAsHex 加密,所以没有用 SpringSecurity 自带的加密方式 BCryptPasswordEncoder。

1、方法 configure(AuthenticationManagerBuilder auth) 解释

代码如下:

@Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userService);
    }

这个方法身份验证,将登录的用户信息放在 auth 中,通过 userDetailsService 方法设置,传入的参数就是之前配置好的 userService。

2、方法 configure(HttpSecurity http) 解释

这个方法用于处理登录表单,主要分为三大块:

  • 登录成功的配置
  • 登录失败的配置
  • 注销登录的配置

2.1 登录成功的配置

protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests().anyRequest().authenticated()
                .and()
                .formLogin()
                .usernameParameter("username")
                .passwordParameter("password")
                .loginProcessingUrl("/doLogin")
                .loginPage("/login")
                .successHandler(new AuthenticationSuccessHandler() {
                    @Override
                    public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
                        httpServletResponse.setContentType("application/json;charset=utf-8");
                        PrintWriter writer = httpServletResponse.getWriter();
                        User user = (User) authentication.getPrincipal();
                        user.setPassword(null);
                        RespBean ok = RespBean.ok("登录成功!", user);
                        String string = new ObjectMapper().writeValueAsString(ok);
                        writer.write(string);
                        writer.flush();
                        writer.close();
                    }
                })
                ......
  }

其中

http.authorizeRequests().anyRequest().authenticated()

表明所有请求都需要登录过后才能访问。

.formLogin()
                .usernameParameter("username")
                .passwordParameter("password")
                .loginProcessingUrl("/doLogin")
                .loginPage("/login")

配置登录表的 用户名、密码、处理登录的页面、登录成功的页面。

登录成功后:

httpServletResponse.setContentType("application/json;charset=utf-8");
                        PrintWriter writer = httpServletResponse.getWriter();
                        User user = (User) authentication.getPrincipal();
                        user.setPassword(null);
                        RespBean ok = RespBean.ok("登录成功!", user);
                        String string = new ObjectMapper().writeValueAsString(ok);
                        writer.write(string);
                        writer.flush();
                        writer.close();

这些配置几乎都是固定的,直接 copy 即可。

还有登录失败的配置、注销登录的配置,可以参考文章:Spring Security 基础教程 -- HttpSecurity 权限和登录表单配置

配置密码加密的类

由于我的数据库中 User 类的密码采用的是 md5DigestAsHex 加密,没有用 SpringSecurity 自带的加密方式 BCryptPasswordEncoder。

所以需要自定义加密类,照样在 config 包下,新建 MyPasswordEncoder 类:

@Component
public class MyPasswordEncoder implements PasswordEncoder {
    @Override
    public String encode(CharSequence charSequence) {
        return DigestUtils.md5DigestAsHex(charSequence.toString().getBytes());
    }

    @Override
    public boolean matches(CharSequence charSequence, String s) {
        return s.equals(DigestUtils.md5DigestAsHex(charSequence.toString().getBytes()));
    }
}

配置登录成功后的页面

由于 SpringSecurity 在登录成功后,会自动跳转页面,而在 Spring Boot + Vue 前后端分离项目中,跳转页面是前端的事,后端只返回 json 数据即可。

所以需要额外配置一下。

在 controller 包下新建类 LoginController :

@RestController
public class LoginController {
    @GetMapping("/login")
    public RespBean login(){
        return RespBean.error("尚未登录,请登录!");
    }
}

测试登录接口

一切准备就绪,需要测试一下效果。

在 controller 包下新建类 HelloController:

@RestController
public class HelloController {

    @GetMapping("/hello")
    public String hello(){
        return "hello";
    }
}

这里定义了一个访问接口 /hello,最后用 postman 测试。

postman 测试

  1. 启动项目,在 postman访问 http://localhost:8081/hello,效果如下:

springboot前后端不分离怎么部署 springboot前后端分离登录怎么做_json

  1. 登录操作

springboot前后端不分离怎么部署 springboot前后端分离登录怎么做_ide_02

  1. 再访问接口 /hello

springboot前后端不分离怎么部署 springboot前后端分离登录怎么做_ide_03

  1. 注销登录

注销登录是 get 请求,默认接口时 logout :

springboot前后端不分离怎么部署 springboot前后端分离登录怎么做_json_04

每天学习一点点,每天进步一点点。