Spring Boot + Vue 前后端分離項目 -- 后端登錄接口實現


前言

在 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,效果如下:

  1. 登錄操作

  1. 再訪問接口 /hello

  1. 注銷登錄

注銷登錄是 get 請求,默認接口時 logout :

每天學習一點點,每天進步一點點。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM