Spring Security&前后端分离项目的使用

整体思路

  1. 前端页面登录,绑定对象,发送axious到后端,进行验证
  2. 后端接收到前端的信息,此时密码为明文,而数据库中密码为密文,只能通过用户名查询数据库,判断用户是否存在,如果存在,通过BCryptPasswordEncoder对密码进行验证
  3. 验证密码通过后,查询用户的角色和权限信息,放到用户对象中,此时只需要字符串即可
  4. 通过私钥进行加密,颁发令牌到前端
  5. 前端接收到登录后的响应,判断是否成功登录,如果成功将token存在localstorage中,然后通过路由跳转页面到首页,登录操作完毕
  6. 此时访问其他资源会报403,因为springsecurity的上下文中并没有用户信息,用户角色和用户的权限,这个工作由springsecurity的FilterSecurityInterceptor这个过滤器来执行,我们需要在basicAuthenticationFilter这个过滤器生效时重写,验证token并将token中的用户信息放到springsecurity的上下文中
  7. 重写basicAuthenticationFilter时注意,此过滤器在FilterSecurityInterceptor之前,先要放行登录和验证请求,必须要return,使用SecurityContextHolder获取context上下文设置认证信息,这个认证信息需要UsernamepasswordAuthenticationToken对象利用构造方法创建,形参token中的载荷,null,authorities,其中的authorities通过AuthoritUtils中的commaSeparatedStringtoAuthorityList获取
  8. 前端发送请求时就需要将正确的token放到请求头中,后端验证之后方可访问资源,在vue的utils下的request.js中请求前从localstorage中获取token,然后设置到请求头中
  9. 此时如若token不正确或过期,无法访问资源,但依然会停留在访问页面,这样的用户体验不好,所以需要增加一个认证环节
  10. 后台认证:同样利用公钥解析请求头中的token,此过程无异常则表示token合法,正常返回一个ResultVO,需要注意的是:该认证的请求也需要在basicAuthenticationFilter和security配置类中放行
  11. 前端认证:在main.js中解开之前注释的permission组件,在permission.js中发送后台认证的请求,通过返回的数据判断token是否有效,有效则让原请求去访问资源,需要注意的是:要判断该请求是否是登录请求,若是登录请求可直接放行,访问登录页面,若token不合法就跳回登录页面

1. 登录认证,颁发令牌

  • 直接使用vue的index.vue界面,改变其中username和password的绑定
<el-form-item prop="username">
        <span class="svg-container">
          <svg-icon icon-class="user" />
        </span>
        <el-input
          ref="username"
          v-model="loginForm.userName"
          placeholder="请输入用户名"
          name="username"
          type="text"
          tabindex="1"
          auto-complete="on"
        />
      </el-form-item>

      <el-form-item prop="password">
        <span class="svg-container">
          <svg-icon icon-class="password" />
        </span>
        <el-input
          :key="passwordType"
          ref="password"
          v-model="loginForm.password"
          :type="passwordType"
          placeholder="请输入密码"
          name="password"
          tabindex="2"
          auto-complete="on"
          @keyup.enter.native="handleLogin"
        />
  • 修改api,发送axious后端登录验证
import request from '@/utils/request'

export function login(data) {
  return request({
    url: '/user/login',
    method: 'post',
    data
  })
}
  • 后端controller层
@PostMapping("login")
    @ApiOperation("用户登录")
    public ResultVO login(@RequestBody SysUser sysUser) {
        return userService.login(sysUser);
    }
  • 后端service层
@Override
    public ResultVO login(SysUser loginUser) {
        try {
            if (loginUser == null) {
                return new ResultVO(false, "用户名或密码不正确");
            }
            // 通过用户名查询用户是否存在,因为密码再数据库中是密文
            SysUser userInDB = userMapper.findUserByUserName(loginUser.getUserName());
            if (userInDB == null) {
                // 用户不存在
                return new ResultVO(false, "用户名或密码不正确");
            }
            // 用户存在,对比密码
            BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
            // 前一个参数是明文密码,后一个是数据库中的密文密码
            boolean matches = encoder.matches(loginUser.getPassword(), userInDB.getPassword());

            if (!matches) {
                // 密码不匹配
                return new ResultVO(false, "用户名或密码不正确");
            }
            // 密码匹配,颁发令牌
            // 1.获取用户的角色信息
            List<String> rolesList = userMapper.findRolesByUserId(String.valueOf(userInDB.getUserId()));
            String roles = "";
            // 遍历集合
            for (String roleName : rolesList) {
                roles += roleName + ",";
            }
            roles = roles.substring(0, roles.length() - 1);
            // 设置用户的角色信息
            userInDB.setRoles(roles);
            // 2.获取用户的权限信息
            List<String> permissionList = userMapper.findPermissionByUserId(userInDB.getUserId());
            String permissions = "";
            // 遍历集合
            for (String permissionName : permissionList) {
                permissions += permissionName + ",";
            }
            permissions = permissions.substring(0, permissions.length() - 1);
            // 设置用户的权限信息
            userInDB.setPermissions(permissions);
            // token载荷中不要放敏感信息
            userInDB.setPassword("");
            // 3.获取密钥
            PrivateKey privateKey = RsaUtils.getPrivateKey(ResourceUtils.getFile("classpath:rsa_pri").getPath());
            // 4. 颁发令牌
            String token = JwtUtils.generateTokenExpireInMinutes(userInDB, privateKey, 45);
            // 5. 没有异常正常返回
            return new ResultVO(true, "登录成功", token);
        } catch (Exception e) {
            e.printStackTrace();
            return new ResultVO(false, "令牌不合法!!!");
        }
    }
  • 后端mapper层
 // 通过用户名查询用户
    @Select("select * from sys_user where user_name = #{userName}")
    public SysUser findUserByUserName(String userName);

    //查询用户的角色信息
    @Select("SELECT role.role_name FROM sys_user_role sur " +
            "LEFT JOIN sys_role role ON sur.role_id = role.role_code " +
            "WHERE sur.user_id = #{userId}")
    public List<String> findRolesByUserId(String userId);

    // 查询用户权限信息
    @Select("SELECT per.perm_name FROM sys_user_role sur \n" +
            "LEFT JOIN sys_role_permission srp ON sur.role_id = srp.role_id\n" +
            "LEFT JOIN sys_permission per ON srp.perm_id = per.perm_code\n" +
            "WHERE sur.user_id = 1;")
    public List<String> findPermissionByUserId(Integer userId);

2. 配置信息

  • SpringSecurity的配置
@Configuration
@EnableWebSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {

    // 放行静态资源
    @Override
    public void configure(WebSecurity web) throws Exception {
        web.ignoring().antMatchers(
                "/swagger-ui.html/**","/css/**","/images/**","/plugins/**","/webjars/**",
                "/swagger-resources/**","/v2/**"
        );
    }

    // 放行系统资源
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                //登录请求必须放行
                .antMatchers("/user/login","/user/verify").permitAll()
                //除了以上资源,剩下的http资源都必须登录后才能访问
                .anyRequest().authenticated();

        //关闭ccrf过滤器
        http.csrf().disable();

        //解决跨域
        http.cors();

        http.addFilter(new MyBasicAuthenticationFilter(authenticationManager()));
    }
}
  • swagger2的配置,需要加入token
@Configuration
@EnableSwagger2
public class Swagger2Configuration {

    private ApiInfo apiInfo() {
        return new ApiInfoBuilder().title("锋迷铺子运营平台接口文档")
                .description("锋迷铺子运营平台接口文档")
                .version("1.0.0")
                .build();
    }
    @Bean
    public Docket createRestApi(Environment environment) {

        Profiles profiles = Profiles.of("dev");

        boolean enable = environment.acceptsProfiles(profiles);

        //添加head参数start
        ParameterBuilder tokenPar = new ParameterBuilder();
        List<Parameter> pars = new ArrayList<Parameter>();
        tokenPar.name("token").description("令牌").modelRef(new ModelRef("string")).parameterType("header").required(false).build();
        pars.add(tokenPar.build());


        return new Docket(DocumentationType.SWAGGER_2)
                .apiInfo(apiInfo())
                .enable(enable)
                .select()
                .apis(RequestHandlerSelectors.basePackage("com.qf")) //这里写的是API接口所在的包位置
                .paths(PathSelectors.any())
                .build()
                .globalOperationParameters(pars);
    }

    private static List<ApiKey> unifiedAuth() {
        List<ApiKey> arrayList = new ArrayList();
        arrayList.add(new ApiKey("Authorization", "token", "header"));
        return arrayList;
    }

}

注意: 此时在登录之后,后端会给前端颁发一个令牌,此后的请求中,前端必须要携带这个令牌进行访问,而后端也需要验证令牌的正确性才能决定是否可以放行访问资源,下面需要做的就是认证的操作

3. 认证:后端操作

  • SpringSecurity中的BasicAuthenticationFilter就是做认证处理的

此过滤器在FilterSecurityInterceptor之前,所以需要先放行登录和认证的请求

此外,在认证之后需要将用户的信息,包括角色和权限都要放到springsecurity的上下文中,这样springsecurity才能起作用

public class MyBasicAuthenticationFilter extends BasicAuthenticationFilter {
    public MyBasicAuthenticationFilter(AuthenticationManager authenticationManager) {
        super(authenticationManager);
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
        try {
            // 因该过滤器在前面,所以要前放行login
            String requestURI = request.getRequestURI();

            if ("/user/login".equals(requestURI)) {
                // 放行
                chain.doFilter(request, response);
                // 必须加return
                return;
            }
            // 从请求头中获取jwt令牌 约定key为token
            String token = request.getHeader("token");

            if (StringUtils.isEmpty(token)) {
                // 直接拦截,响应给客户端
                response(response, new ResultVO(false, "令牌不能为空"));
            }
            // token不为空,利用公钥解密token获取用户信息
            // 获取公钥
            PublicKey publicKey = RsaUtils.getPublicKey(ResourceUtils.getFile("classpath:rsa_pub").getPath());
            // 解密token
            SysUser info = (SysUser) JwtUtils.getInfoFromToken(token, publicKey, SysUser.class);
            // 获取用户的authorities
            String authority = info.getRoles() + "," + info.getPermissions();
            // 利用工具类获取
            List<GrantedAuthority> authorities = AuthorityUtils.commaSeparatedStringToAuthorityList(authority);

            UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken =
                    new UsernamePasswordAuthenticationToken(info, null, authorities);

            // 将用户的信息放到spring security的上下文中
            SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);

            // 放行
            chain.doFilter(request, response);
        } catch (Exception e) {
            e.printStackTrace();
            response(response,new ResultVO(false,"无效令牌!!!"));
        }


    }

    private void response(HttpServletResponse response, ResultVO res) {
        try {
            response.setContentType("application/json;charset=utf-8");
            response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
            PrintWriter out = response.getWriter();

            out.write(JSONUtil.toJsonPrettyStr(res));
            out.flush();
            out.close();
        } catch (Exception e) {

        }

    }
}
  • 认证的配置
 // 放行系统资源
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                //登录请求必须放行
                .antMatchers("/user/login","/user/verify").permitAll()
                //除了以上资源,剩下的http资源都必须登录后才能访问
                .anyRequest().authenticated();

        //关闭ccrf过滤器
        http.csrf().disable();

        //解决跨域
        http.cors();
        
        // 此为配置我们自己写的basicAuthenticationFilter过滤器
        http.addFilter(new MyBasicAuthenticationFilter(authenticationManager()));

有了后端认证之后,前端发的请求中必须要携带正确的token才能访问资源

4.认证:前端操作

  • 每次请求需要将token加入到请求头中,并按照约定的key去设置token
//  请求拦截器
service.interceptors.request.use(
  config => {
    // 将token放入请求头中
    var tokenVal = localStorage.getItem('token')
    config.headers.token = tokenVal
    return config
  },
  error => {
    return Promise.reject(error)
  }
)

  • 前端的请求实则是通过路由进行页面跳转,我们需要在permission.js中去修改拦截器,前提:在main.js中解开之前注释的permission组件
/**
 * 在路由跳转之前,要将token放到请求头中,后台才能做验证
 */
router.beforeEach(async(to, from, next) => {
  // start progress bar
  NProgress.start()

  // set page title
  document.title = getPageTitle(to.meta.title)

  verify().then(res => {
    if (!res.success) {
      if (to.path === '/login') {
        next()
      } else {
        // 跳回登录页面
        next(`/login?redirect=${to.path}`)
        NProgress.done()
      }
    } else {
      // 放行
      next()
    }
  })
})

  • 3
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值