一文搞定SpringSecurity+vue前後端分離

我好菜啊,學了好幾天才明白一點點

前言

  把v部落git下來學一學,比起halo來說v部落會更加簡單好懂一點。我看他用了SpringSecurity來做登錄驗證,那第一步就是學學這個SpringSecurity。
  然後我就發現了,我真的是太菜了,看博客,看視頻都不盡如意。尤其是用vue配合SpringSecurity的情況下實在是費勁,看了好多資料感覺都不是我需要的,懂得人感覺特別簡單,不懂的就很費勁,說來說去還是我太菜了。
  我也是看了好幾天資料並結合v部落的代碼,纔算是琢磨出來一點點門道,所以就記錄一下我這個學習的成果。主要是vue配合SpringSecurity來使用,雙方互相用json傳遞數據。學習之前需要懂得以下技術:

  • Springboot
  • MyBatis
  • Vue,Axios,vue-router
  • ······
  • 其他零零散散的我就不說了

開發準備

  首先我們需要有以下兩三個頁面:

  • 登錄頁面,
  • 主頁
    • 用戶管理頁面==>>管理員身份才能訪問
    • 文章管理頁面==>>普通用戶才能訪問

  這幾個頁面我是用vue寫的,大家有時間也可以自己寫寫,當然部分代碼我也是參考別人的,雖然有那麼一點點缺陷,但不影響使用。我把頁面放在我的碼雲上面,不想寫的話可以git下來。
碼雲地址:https://gitee.com/siumu/blog_code.git
界面長這個樣子:

在這裏插入圖片描述
在這裏插入圖片描述


後端開發

創建項目

  接下來就是準備後端的代碼了,先創建一個項目,再建立一個數據庫。剛開始自然是創建項目,在pom文件裏把Spring Boot,SpringSecurity,MyBatis等等一些東西,以下就是我的pom依賴:

    <dependencies>
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>2.1.2</version>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.1.47</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
            <exclusions>
                <exclusion>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-starter-tomcat</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <!-- 使用undertow替換tomcat -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-undertow</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
            <exclusions>
                <exclusion>
                    <groupId>org.junit.vintage</groupId>
                    <artifactId>junit-vintage-engine</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-test</artifactId>
            <scope>test</scope>
        </dependency>
        <!-- hutool開源JSON工具 -->
        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-json</artifactId>
            <version>5.2.4</version>
        </dependency>
    </dependencies>

這裏面有的是自帶的,有的是我後來加上去的。也沒啥複雜的東西。

建立數據庫

  既然是用戶角色權限控制,那自然就需要有一個用戶表,一個角色表。根據v部落設計的數據庫,一個用戶可以有多個角色,所以用戶與角色之間就是多對多的關係,那麼就需要一個用戶角色關係表。
  總結一下就是需要三張表分別是用戶表、角色表、用戶角色關係表。
在這裏插入圖片描述
  數據庫我也放在碼雲上,直接導入sql文件即可

創建實體類

  接下來自然是建立實體類,跟數據庫一一對應,這裏也不復雜介紹,反正用的是v部落的數據庫,看看字段註釋就知道啥意思了。
  首先是用戶實體類

@Data
public class User implements UserDetails {
    private Long id;             //主鍵
    private String username;     //用戶名
    private String password;     //密碼
    private String nickname;     //暱稱
    private boolean enabled;     //是否禁用
    private List<Role> roles;    //用戶角色
    private String email;        //郵箱
    private String userface;     //頭像
    private Timestamp regTime;   //註冊時間
    
    @Override
    public List<GrantedAuthority> getAuthorities() {
        List<GrantedAuthority> authorities = new ArrayList<>();
        for (Role role : roles) {
            authorities.add(new SimpleGrantedAuthority("ROLE_" + role.getName()));
        }
        return authorities;
    }
    @Override
    public boolean isAccountNonExpired() {
        return true;
    }
    @Override
    public boolean isAccountNonLocked() {
        return true;
    }
    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

}

  爲什麼要實現UserDetails這個接口呢?如果瞭解過這方面的知識就會知道,使用SpringSecurity從數據庫裏拿用戶信息需要實現這個接口,這個接口,提供了一系列的方法,比如賬戶是否過期啊,用戶是否被鎖定啊等等。
  其中有些字段我們數據庫裏有,比如用戶名,密碼,是否禁用之類的。但是有些就沒有,所以我們需要重寫這些方法,然後讓他們統統返回true。如果不瞭解可以百度一下這方面的博客看一看,我們現在直接用它就行了。
  然後是角色實體類

@Data
public class Role {
    /**
     * 主鍵
     */
    private Long id;

    /**
     * 角色名稱
     */
    private String name;

}

實體類就這麼簡單的完成了。
接下來就是配置數據庫連接了,這應該簡單,我就直接把application.yml放上來吧。

spring:
  datasource:
    type: com.zaxxer.hikari.HikariDataSource
    driver-class-name: com.mysql.jdbc.Driver
    url: jdbc:mysql://localhost:3306/dbgirl?characterEncoding=utf8&useSSL=false
    username: root
    password: 123456
server:
  port: 8080

創建mapper

  接下來就是寫操作數據庫部分了,大家熟悉什麼就用什麼,我是看MyBatis比較火,所以我也用MyBatis。
  以下是mapper層的接口與xml:

public interface UserMapper {
    /**
     * 根據用戶名查詢用戶信息
     * @param username 用戶名
     * @return
     */
    User selectByUserName(String username);
}
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.xiumu.securitydemo.mapper.UserMapper">
    <resultMap id="UserAndRole" type="com.xiumu.securitydemo.model.pojo.User">
        <id column="id" property="id"/>
        <result column="username" property="username"/>
        <result column="nickname" property="nickname"/>
        <result column="password" property="password"/>
        <result column="enabled" property="enabled"/>
        <result column="email" property="email"/>
        <result column="userface" property="userface"/>
        <result column="regTime" property="regTime"/>
        <collection property="roles" ofType="com.xiumu.securitydemo.model.pojo.Role">
            <id column="rid" property="id"/>
            <result column="name" property="name"/>
        </collection>
    </resultMap>
    <sql id="UserAndRole">
        u.*,r.id rid,r.name
    </sql>
    <select id="selectByUserName" parameterType="string" resultMap="UserAndRole">
        select <include refid="UserAndRole"/>
        FROM user u 
        LEFT JOIN roles_user ru ON u.id = ru.uid 
        LEFT JOIN roles r ON ru.rid = r.id
        WHERE u.username = #{username}
    </select>
</mapper>

  多表連接查詢就不細說了,大家肯定能看懂。無非就是根據用戶名查詢一條用戶記錄。

創建service

  接下來就是業務層。現在不都是說要面向接口編程嘛,那咱就先建立一個接口,繼承UserDetailsService,爲什麼繼承這個接口呢,瞭解過的話就會知道,SpringSecurity就是用這個接口的loadUserByUsername方法來從數據庫獲取信息,寫個類實現這個接口重寫方法就完事。

public interface UserService extends UserDetailsService {
}

@Service
public class UserServiceImpl implements UserService {

    @Resource
    private UserMapper userMapper;

    @Override
    public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
        return userMapper.selectByUserName(s);
    }
}

創建controller

  接下來就是創建控制層了,這個也是簡單寫兩個就行了,就是處理文章管理和用戶管理的兩個請求。當然在此之前我們還是要寫一個通用類,用來當做返回值。

@Data
public class ResultJSON {
    /**
     * 返回的狀態碼
     */
    private Integer code;

    /**
     * 返回信息
     */
    private String msg;


    /**
     * 返回的數據
     */
    private Object result;

    public ResultJSON() {
    }

    /**
     * 只返回狀態碼
     *
     * @param code 狀態碼
     */
    public ResultJSON(Integer code) {
        this.code = code;
    }

    /**
     * 不返回數據的構造方法
     *
     * @param code 狀態碼
     * @param msg  信息
     */
    public ResultJSON(Integer code, String msg) {
        this.code = code;
        this.msg = msg;
    }

    /**
     * 返回數據的構造方法
     *
     * @param code   狀態碼
     * @param msg    信息
     * @param result 數據
     */
    public ResultJSON(Integer code, String msg, Object result) {
        this.code = code;
        this.msg = msg;
        this.result = result;
    }

    /**
     * 返回狀態碼和數據
     *
     * @param code   狀態碼
     * @param result 數據
     */
    public ResultJSON(Integer code, Object result) {
        this.code = code;
        this.result = result;
    }
}

  接下來開始寫正式的controller。

@RestController
public class UserController {

    @GetMapping("/hello")
    public ResultJSON hello(){
        return new ResultJSON(2000,"hello blog!");
    }

    @GetMapping("/vip")
    public ResultJSON vip(){
        return new ResultJSON(2000,"hello 超級管理員!");
    }

}

普通用戶只能訪問這個/hello請求,管理員才能訪問這個/vip請求。

SpringSecurity配置

  接下來就是重點內容了,如何讓SpringSecurity只跟vue返回json數據。而且除了這些,前後端分離也有很多問題,比如跨域問題啊之類的。這些都需要考慮。
  對於跨域,我就很暴力了,直接統統允許就完事。且看如下代碼:

@Configuration
public class CorsConfig implements WebMvcConfigurer {
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**")
                .allowedOrigins("*")
                .allowCredentials(true)
                .allowedMethods("GET","POST")
                .maxAge(3600);
    }
}

  跨域問題解決了,接下來就是SpringSecurity的配置了。我們可以慢慢來,一步步的配置。
  首先就是新建一個配置類,繼承WebSecurityConfigurerAdapter類。重寫以下兩個方法。

@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private UserService userService;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .antMatchers("/hello").hasRole("普通用戶")
                .antMatchers("/vip").hasRole("超級管理員")
                .anyRequest().authenticated()
                .and().cors().and().csrf().disable();
    }

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

我們看到這有兩個方法,有着不同的作用,根據我這幾天的半吊子理解,簡單的說說

  • configure(HttpSecurity http),這個方法的主要作用就是設置某個請求需要什麼權限才能訪問,例如這裏我設置/hello由普通用戶這個角色訪問,/vip由超級管理員這個角色訪問。當然這只是基本的作用,還有很多其他的作用,比如跨域登錄請求啊等等。
  • configure(AuthenticationManagerBuilder auth),這個方法的主要作用就是設置用戶的權限,從數據庫獲取用戶信息之類的。比如這裏,我將userService傳給它,它就能自動的從數據庫獲取用戶的信息。

到這裏並沒有結束,接下來我們需要將這些配置一一完善。

密碼加密配置

  新版本里不僅要配置用戶的userService,還要配置passwordEncoder,也就是密碼的加密解密。既然如此我們就實現一個PasswordEncoder接口好了。當然這也是我參考人家v部落的。代碼如下。

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

    /**
     * @param charSequence 明文
     * @param s 密文
     * @return
     */
    @Override
    public boolean matches(CharSequence charSequence, String s) {
        return s.equals(DigestUtils.md5DigestAsHex(charSequence.toString().getBytes()));
    }
}

然後把這個實現類配置到上面說的那個configure(AuthenticationManagerBuilder auth)方法裏。

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

到這裏並沒有結束。

登錄請求配置

  接下來就是配置登錄請求的url,並允許不登錄訪問,我就鬧過一次笑話,測試的時候沒有開啓這個不登錄訪問,結果登錄測試居然給我返回我沒有登錄。
  也是一行代碼就搞定的事情,在上面說的這個configure(HttpSecurity http)方法裏。

@Override
protected void configure(HttpSecurity http) throws Exception {
    http.authorizeRequests()
            .antMatchers("/hello").hasRole("普通用戶")
            .antMatchers("/vip").hasRole("超級管理員")
            .anyRequest().authenticated()
            .and().cors().and().csrf().disable()
            .formLogin().loginProcessingUrl("/login").permitAll();
}

到這裏還沒有結束。
  這個登錄請求url配置完了,那我們登錄的時候只需要訪問這個url就行了。但是現在還是有問題,就是登錄失敗會直接給你返回一個登錄頁面,並不能給你返回一個json數據,我們想要的是它登錄失敗或者登錄成功都應該是一個json數據返回給前端。

設置登錄成功或失敗返回JSON數據

  這個問題人家自然是考慮到了,所以能夠設置登錄成功處理與登錄失敗處理。我們只需要實現人家的登錄認證成功接口與登錄認證失敗接口,再將其配置進去就可以了。
  那麼我們就需要再寫幾個類,首先是封裝一下登錄失敗或者登錄成功的返回信息,當然我們把沒有登錄啊,沒有權限啊,註銷登錄的返回信息都封裝一下。

public class ResultArgsUtil {
    //登錄驗證失敗
    public static String USER_NOT_EXIST_FAILURE_MSG = "賬號或者密碼錯誤";
    public static Integer USER_NOT_EXIST_FAILURE_CODE = 1004;

    //沒有登錄
    public static String USER_NOT_LOGIN_FAILURE_MSG = "未登錄";
    public static Integer USER_NOT_LOGIN_FAILURE_CODE = 1002;

    //登錄成功
    public static String USER_LOGIN_SUCCESS_MSG = "登錄成功";
    public static Integer USER_LOGIN_SUCCESS_CODE = 1000;

    //無權限
    public static String AUTHORIZE_FAILURE_MSG = "沒有權限";
    public static Integer AUTHORIZE_FAILURE_CODE = 1003;

    //註銷成功
    public static String LOGOUT_SUCCESS_MSG = "註銷成功";
    public static Integer LOGOUT_SUCCESS_CODE = 1005;

}

  我們既然要返回json數據,那肯定要設置響應頭,把返回對象轉成json數據返回給前端。這幾個步驟是重複的,唯一不重複的就是返回的對象不一樣,所以我們就再封裝一個工具類,就是返回json數據的通用方法。

public class SecurityHandlerUtil {
    /**
     * security處理返回結果
     * @param response 響應
     * @param result 結果
     * @throws IOException
     */
    public static void responseHandler(HttpServletResponse response, ResultJSON result) throws IOException {
        response.setContentType("application/json;charset=utf-8");
        PrintWriter writer = response.getWriter();
        writer.write(JSONUtil.toJsonStr(result));
        writer.flush();
        writer.close();
    }
}

接下來就是正式寫登錄認證成功或失敗的接口實現類。

public class LoginSuccessHandlerImpl implements AuthenticationSuccessHandler {
    @Override
    public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
        SecurityHandlerUtil.responseHandler(httpServletResponse,new ResultJSON(USER_LOGIN_SUCCESS_CODE,USER_LOGIN_SUCCESS_MSG));
    }
}
public class LoginFailureHandlerImpl implements AuthenticationFailureHandler {
    @Override
    public void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
        SecurityHandlerUtil.responseHandler(httpServletResponse,new ResultJSON(USER_NOT_EXIST_FAILURE_CODE,USER_NOT_EXIST_FAILURE_MSG));
    }
}

然後再將這兩個類配置到上面configure(HttpSecurity http)這個方法裏。

 @Override
 protected void configure(HttpSecurity http) throws Exception {
     http.authorizeRequests()
             .antMatchers("/hello").hasRole("普通用戶")
             .antMatchers("/vip").hasRole("超級管理員")
             .anyRequest().authenticated()
             .and().cors().and().csrf().disable()
             .formLogin().loginProcessingUrl("/login").permitAll()
             .successHandler(new LoginSuccessHandlerImpl())
             .failureHandler(new LoginFailureHandlerImpl());
 }

這當然還沒完。

無權限,註銷,未登錄

  除了上面那個登錄成功與失敗處理,這些沒有權限啊,未登錄啊,註銷啊,之類的都需要我們自己來配置一下。所以接下來我們就寫一寫這些情況下的處理類,當然每個類都要實現人家的接口。
  首先是註銷的處理類。

public class LogoutHandlerImpl implements LogoutSuccessHandler {
    @Override
    public void onLogoutSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
        SecurityHandlerUtil.responseHandler(httpServletResponse,new ResultJSON(LOGOUT_SUCCESS_CODE,LOGOUT_SUCCESS_MSG));
    }
}

  然後是沒有權限的處理類。

public class AccessDeniedHandlerImpl implements AccessDeniedHandler {
    @Override
    public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AccessDeniedException e) throws IOException, ServletException {
        SecurityHandlerUtil.responseHandler(httpServletResponse,new ResultJSON(AUTHORIZE_FAILURE_CODE,AUTHORIZE_FAILURE_MSG));
    }
}

  接下來就是沒有登錄的處理類。

public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint {
    @Override
    public void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
        SecurityHandlerUtil.responseHandler(httpServletResponse,new ResultJSON(USER_NOT_LOGIN_FAILURE_CODE,USER_NOT_LOGIN_FAILURE_MSG));
    }
}

  然後我們把這些實現類都配置到上面configure(HttpSecurity http)這個方法裏。如此纔是全部的配置。

@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private UserService userService;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .antMatchers("/hello").hasRole("普通用戶")
                .antMatchers("/vip").hasRole("超級管理員")
                .anyRequest().authenticated()
                .and().cors().and().csrf().disable()
                .formLogin().loginProcessingUrl("/login").permitAll()
                .successHandler(new LoginSuccessHandlerImpl())
                .failureHandler(new LoginFailureHandlerImpl())
                .and()
                .logout().logoutSuccessHandler(new LogoutHandlerImpl()).permitAll()
                .and()
                .exceptionHandling()
                .accessDeniedHandler(new AccessDeniedHandlerImpl())
                .authenticationEntryPoint(new AuthenticationEntryPointImpl());
    }

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

可能遇到的問題

  • 跨域問題
  • vue每次請求的JSESSIONID不一致問題。這個問題也比較好解決就是在axios中設置withCredentials屬性爲true就可以。 在這裏插入圖片描述

項目效果

以下是項目運行的效果圖。在這裏插入圖片描述
前後端代碼都會放在碼雲上。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章