一文搞定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
界面长这个样子:

一文搞定SpringSecurity+vue前后端分离_第1张图片
一文搞定SpringSecurity+vue前后端分离_第2张图片


后端开发

创建项目

  接下来就是准备后端的代码了,先创建一个项目,再建立一个数据库。刚开始自然是创建项目,在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部落设计的数据库,一个用户可以有多个角色,所以用户与角色之间就是多对多的关系,那么就需要一个用户角色关系表。
  总结一下就是需要三张表分别是用户表、角色表、用户角色关系表。
一文搞定SpringSecurity+vue前后端分离_第3张图片
  数据库我也放在码云上,直接导入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就可以。 一文搞定SpringSecurity+vue前后端分离_第4张图片

项目效果

以下是项目运行的效果图。一文搞定SpringSecurity+vue前后端分离_第5张图片
前后端代码都会放在码云上。所有用户的密码都是123

你可能感兴趣的:(开源项目学习周边,java,spring,boot)