项目日记(在线办公项目)day2021/02/23

项目日记day0223

一、没有加入SpringSecurity前的跨域

项目的环境搭建好之后,出现了各种各样的问题,我遇到的第一个问题便是跨域问题:

因为是前后端分离的项目,所以跨域是必不可少的。

项目开始之前,我分别在前端和后端进行跨域配置:

前端跨域主要是对axios的原型中进行配置,在main.js中加入如下代码:

//将axios注册为Vue的一个原型属性
Vue.prototype.$axios= axios.create({
  baseURL: 'http://localhost/jiazhong-office',
  withCredentials: true, // 允许使用凭证
  headers: {'X-Requested-With': 'XMLHttpRequest'}//设置请求头为ajax请求
});

后端的跨域是在启动类中,进行配置:

@Bean
    public WebMvcConfigurer webMvcConfigurer(){
        return new WebMvcConfigurer() {
            /***
             * 添加跨域配置
             * @param registry
             */
            @Override
            public void addCorsMappings(CorsRegistry registry) {
                registry.addMapping("/**")//允许接收跨域访问的路径(所有文件)
                        .allowedMethods("*")//允许跨域访问的请求方法(所有请求)
                        .allowCredentials(true)//允许使用凭证(session)
                        .allowedOrigins("http://localhost:8080");//允许跨域访问本应用的路径

            }
        };
    }

配置完毕之后,我们先在前端写一个测试的视图,进行跨域访问的测试:

<template>
    <div>
        <button @click="test">测试</button>
    </div>
</template>

<script>
export default {
    data(){
        return{}
    },
    methods:{
        test(){
            this.$axios.get("/test")
                        .then(response=>{
                            alert(response.data);
                        })
                        .catch(err=>{
                            alert(err);
                        })
        }
    }
}
</script>

我们写的是一个测试的按钮,当点击该按钮时,访问后端的“/test”路径,所以我们需要在后端编写一个"/test"路径的controller:

@RestController
public class Test {
    @GetMapping("/test")
    public String test(){
        return "test";
    }
}

编写完成之后,访问前端地址:http://localhost:8080/test,点击按钮,不出我们所料,弹出来controller返回的字符串“test”。

二、加入SpringSecurity后的跨域

我们首先要实现的功能便是登陆功能,这里我们选择SpringSecurity进行认证登录功能的设计。

首先我们要加入SpringSecurity的maven 依赖:

<!--spring-boot-starter-security-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
    <version>2.3.8.RELEASE</version>
</dependency>

加入依赖后,我们再点击按钮,发现出现了错误:

在这里插入图片描述

这个问题为跨域错误,也就是说,我们在加入了SpringSecurity后,又出现了跨域问题。

我们解决该问题的方法是,在SpringSecurity的配置类中,给后端的"/test"不需要凭证可以直接访问:

@Override
protected void configure(HttpSecurity http) throws Exception {
    http.
        authorizeRequests()
        .antMatchers("/test") //拦截的路径
        .permitAll();//设置不需要凭证可以直接访问
}

这时我们再来测试一下:

在这里插入图片描述

测试成功!

三、在一个项目中编写登录页面进行登录测试

我们现在开始进行登陆测试,我们先在后端项目中,建立登录页面(login.html)、首页(index.html)、登陆失败页面(fail.html)。

login.html

<form action="login" method="post">
    账号:<input type="text" name="username"/><br/>
    密码:<input type="password" name="password"/><br/>
    <button>登录</button>
</form>

此处有一个小坑,因为我们在SpringBoot的配置文件中,说明了后端项目名称,所以访问的时候需要加一个后端项目名才可以进行后端项目的访问,所以action属性的值必须为"login",而不能为"/login"。如果为"/login",后端做登陆处理时的controller路径为“http://localhost/login”,而正确的路径为"http://localhost/项目名/login"。

index.html

<h1 align="center">首页</h1>

fail.html

<h1>登陆失败....</h1>

然后进行SpringScurity的自定义登陆页面的配置:

@Override
    protected void configure(HttpSecurity http) throws Exception {
        http.
                authorizeRequests()
                    .antMatchers("/test")
                        .permitAll()
              .and()
                .formLogin() //登陆表单配置
                    .loginPage("/login.html") //指定登陆表单的页面
                    .loginProcessingUrl("/login") //指定处理登录操作的地址,此处不访问项目名,因此可以直接加/login
                    .defaultSuccessUrl("/index.html") //指定登陆成功后的页面地址
                    .failureUrl("/fail.html"); //指定登录失败后的页面地址

我们访问后端首页地址:“http://localhost/jiazhong-office”,页面跳转到"http://localhost/jiazhong-office/login.html",这符合我们预期的想法,但是却出现了找不到页面的情况,错误原因是“重定向次数过多”。

在这里插入图片描述

通过查阅资料,我们发现,这是因为我们没有在SpringScurity配置"login.html"不进行拦截,所以SpringScurity对"login.html"进行了拦截,所以我们需要在antMatchers中,加入"login.html",为了之后登陆失败处理方便,我们也在其中加上登陆失败的地址"fail.html"。

@Override
protected void configure(HttpSecurity http) throws Exception {
    http.
        authorizeRequests()
        .antMatchers("/test","/login.html","/fail.html")
        .permitAll()
        .and()
        .formLogin()
        .loginPage("/login.html")
        .loginProcessingUrl("/login")
        .defaultSuccessUrl("/index.html")
        .failureUrl("/fail.html");
}

我们再次访问后端首页地址,访问成功!

在这里插入图片描述

我们知道,在SpringScurity中有内置账户,我们先利用内置账户来进行登陆测试。

我们输入账户名"user",密码后点击登录按钮,此时出现了一个情况:登陆页面又刷新了一遍。
在这里插入图片描述

我们想要的结果是,按钮后,跳转到首页,此时却出现了这样的问题。

我们查阅资料后发现,原来我们没有进行禁用跨域伪造的设置,我们在SpringScurity中设置一下:

 @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.
                authorizeRequests()
                    .antMatchers("/test","/login.html","/fail.html")
                        .permitAll()
              .and()
                .formLogin()
                    .loginPage("/login.html")
                    .loginProcessingUrl("/login")
                    .defaultSuccessUrl("/index.html")
                    .failureUrl("/fail.html")
                .and()
                    .csrf()
                        .disable()//禁用跨域伪造
        ;

此时我们再进行登录测试,成功跳转到首页!

在这里插入图片描述

输入错误密码后,跳转到登录失败的页面。

四、去除后端的跨域拦截器

我们再做这么一个测试,首先在测试控制器中,加入另外一个"/test1",代码如下:

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

前端项目中,在测试视图中再加入一个测试"test1"的按钮:

<template>
    <div>
        <button @click="test">测试</button>
        <button @click="test1">测试</button>
    </div>
</template>

<script>
export default {
    data(){
        return{}
    },
    methods:{
        test(){
            this.$axios.get("/test")
                        .then(response=>{
                            alert(response.data);
                        })
                        .catch(err=>{
                            alert(err);
                        })
        },
        test1(){
            this.$axios.get("/test1")
                        .then(response=>{
                            alert(response.data);
                        })
                        .catch(err=>{
                            alert(err);
                        })
        }
    }
}
</script>

我们访问:“http://localhost:8080/#/test”,点击第二个测试按钮,发现还是出现了跨域错误,点击第一个测试按钮,发现测试成功,现在我们来探究一下这其中的原因。

在这里插入图片描述

因为SpringScurity是基于过滤链实现的,而在后端进行的跨域配置是基于"跨域拦截器"实现的,过滤链优于跨域拦截器,所以请求先经过过滤链,再经过跨域拦截器。

当两者都进行了跨域配置后,test不经过过滤链,直接到跨域拦截器,跨域拦截器放行。

当只配置了过滤链,test1会首先经过过滤链,过滤链发现其中没有配置test1路径,则不让其通过,直接pass掉,所以跨域拦截器就失去了它的作用。

综上所述,当开启了SpringScurity后,后端配置的跨域就失去了它的作用,因为过滤链是优于跨域拦截器的,所以我们不妨去掉跨域拦截器的配置。

使用了SpringScurity,需要在SpringScurity中设置跨域:

首先,在SpringScurity的配置类中加入一个SpringSecurity的跨域设置的方法,将其返回的配置资源对象交给Spring进行管理:

/**
     * SpringSecurity的跨域设置
     * SpringSecurity自动在Spring的Bean容器中查询名为"corsConfigurationSource"的Bean进行跨域设置
     *
     * @return 配置资源对象
     */
    @Bean
    public CorsConfigurationSource corsConfigurationSource(){
        //创建一个跨域配置器对象
        CorsConfiguration corsConfiguration = new CorsConfiguration();
        /**
         * 在跨域配置器中进行跨域的相关配置
         */
        //允许使用凭证
        corsConfiguration.setAllowCredentials(true);
        //设置允许访问请求的方法
        corsConfiguration.setAllowedMethods(Arrays.asList("POST","GET","DELETE","PUT","OPTION"));
//        //设置允许访问请求的方法
//        corsConfiguration.addAllowedMethod("*");
        //设置允许跨域访问本应用的路径
        corsConfiguration.setAllowedOrigins(Arrays.asList("http://localhost:8080"));
        //设置未进行配置的项使用默认的配置
        corsConfiguration.applyPermitDefaultValues();

        /**
         * 创建基于URL的配置资源对象,将以上配置好的跨域配置应用到某个资源上
         */
        UrlBasedCorsConfigurationSource corsConfigurationSource = new UrlBasedCorsConfigurationSource();
        //设置应用的根路径及其子路径使用corsConfiguration设置的跨域配置
        corsConfigurationSource.registerCorsConfiguration("/**",corsConfiguration);
        return corsConfigurationSource;
    }

然后在http中,开启允许跨域访问设置:

@Override
    protected void configure(HttpSecurity http) throws Exception {
        http.
                cors() //设置Security允许跨域访问(默认不允许)
              .and()
                .authorizeRequests()
                    .antMatchers("/test","/login.html","/fail.html")
                        .permitAll()
              .and()
                .formLogin()
                    .loginPage("/login.html")
                    .loginProcessingUrl("/login")
                    .defaultSuccessUrl("/index.html")
                    .failureUrl("/fail.html")
                .and()
                    .csrf()
                        .disable()//禁用跨域伪造
        ;

        super.configure(http);
    }

这时,刷新测试页面,点击测试按钮2,出现了401错误,代表没有访问凭证(因为此时没有登陆,只有登陆后才有访问凭证),因为这是前端的页面,发送的是一个ajax请求,返回的也是一个ajax结果,所以会报错。

代表我们此时,跨域设置成功!

在这里插入图片描述

我们把登录页面改为前端的登陆页面:

在这里插入图片描述

这时,访问一个后端的controller请求:“http://localhost/jiazhong-office/test1”,页面会直接跳转到前端的登陆页面"http://localhost:8080/#/"进行登录请求,获取凭证访问资源。
在这里插入图片描述

我们可以做一个测试,将corsConfigurationSource的名字进行改变,再进行登录测试,发现又出现了“Error: Network Error”的跨域错误,所以说明了“SpringSecurity自动在Spring的Bean容器中查询名为"corsConfigurationSource"的Bean进行跨域设置”,名称错误之后就会导致配置失效。

那么如果不使用默认的名称该怎么设置呢?

因为没有使用默认的名称,所以也不需要交给Spring进行管理了,所以我们去掉方法上的@Bean注解。

然后在http的方法中设置手动配置跨域。

在这里插入图片描述

在这里插入图片描述

此时,进行登陆测试,输入正确的账户密码,发现登录成功!

但是,我们还是将跨域资源交给Spring进行管理,由SpringScurity自动配置,毕竟谁都不想多此一举!

五、利用内置账户进行登录

因为我们在前端设置的登陆页面的表单参数不是SpringScurity提供的默认参数(username、password),所以我们要对SpringScurity进行登陆表单的参数设置:

在这里插入图片描述

在这里插入图片描述

接下来,我们开始做登陆的axios请求:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-b268heYF-1614144531626)(项目日记.assets/image-20210223183318060.png)]

因为axios请求是基于“ajax”请求的,所以必须要以ajax方式来响应,但此时登陆成功后,返回的为一个重定向的网址,这显然是不行的。
在这里插入图片描述

所以,我们需要做一些特殊的处理,登陆成功后让其访问一个控制器,控制器返回json响应。

在controller包的rabc包(权限控制包)下,新建一个登陆处理类(LoginHandler),定义登录成功和登录失败的处理方法。

package com.jiazhong.office.controller.rbac;

import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @ClassName: LoginHandler 登录控制器
 * @Description: TODO 定义一些登陆后的操作
 * @Author: JiaShiXi
 * @Date: 2021/2/23 18:37
 * @Version: 1.0
 **/

@RestController
@RequestMapping("/login")
public class LoginHandler {

    /**
     * 登陆成功后的操作
     * @return
     */
    @RequestMapping("/result/loginSuccess")
    public String loginSuccess() {
        return "登陆成功";
    }

    /**
     * 登录失败后的操作
     * @return
     */
    @RequestMapping("/result/loginFail")
    public String loginFail(){
        return "登陆失败";
    }
}

然后在SpringScurity配置类中,修改登陆成功和登陆失败后访问的url。因为登录失败后是没有凭证的,所以需要将登陆失败的url进行无凭证放行的设置。

在这里插入图片描述

这时,我们进行登录测试,进入登陆页面,输入内置账户名和密码:

在这里插入图片描述

此时显示登陆失败,但我们输入的账户名和密码,经过调试窗口查看,明明是正确的。

我们发现,此时传递数据的方式为Request Payload方式,这种方式的传参是以整个请求体来传递的,而经过查看源码,发现账号和密码是通过getParamter的方式来获取的,而这种方式是以单独的参数来传递,不支持Request Payload类型的参数。
在这里插入图片描述

通过查阅资料,我们可以将Request Payload格式变为FormData格式进行提交。

我们可以在前端使用qs插件的stringify()方法将Request Payload传递的数据进行序列化,转换为FormData格式。

所以,我们需要安装qs插件,我们在配置环境的时候,已经把qs插件安装了,所以可以直接进行引用。

因为我们只在登录视图中使用qs,所以没必要在main.js中全局引用,所以只在index.vue中引用即可。引用完后,使用stringify(this.account)方法进行转换。

在这里插入图片描述

这时我们再进行登陆测试,输入正确的内置账号密码,显示登陆成功,而且传递参数的方式也变为了"Form Data"。

在这里插入图片描述

六、SpringScurity中基于数据库用户的认证

在SpringScurity中通过UserDetailsService接口获得认证信息,将认证信息提交给UserDetails,实现基于数据库用户的认证。而UserDetails是一个接口,真正处理的是该接口的实现类,UserDetails接口的实现类由SpringScurity提供。

打开UserDetailsService接口,有一个如下的方法:

UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;

该方法的作用是:通过用户名来加载用户。

打开UserDetails接口,有如下的方法:

/**
	 * 返回授予用户的权限 不能返回空值
	 *
	 * @return 权限,按照自然键排序 (不能为null)
	 */
	Collection<? extends GrantedAuthority> getAuthorities();

	/**
	 * 返回用于验证用户的密码
	 *
	 * @return 用户密码
	 */
	String getPassword();

	/**
	 * 返回用于验证用户的用户名 不能为null
	 *
	 * @return 用户名(不能为null)
	 */
	String getUsername();

	/**
	 * 表示用户的帐户是否已过期,一个过期的用户无法通过身份验证
	 *
	 * @return true表示用户的账户没有过期,false表示用户账户已过期
	 */
	boolean isAccountNonExpired();

	/**
	 * 表示用户的帐户是否已经被锁定,一个锁定的用户无法通过身份验证
	 *
	 * @return true表示用户没有被锁定, false表示用户被锁定
	 */
	boolean isAccountNonLocked();

	/**
	 * 表示用户的凭证(密码)是否已经过期,过期的凭证无法通过身份验证
	 *
	 * @return true表示用户凭证是可用的,false表示用户凭证是不可用的
	 */
	boolean isCredentialsNonExpired();

	/**
	 * 表示用户状态是可用还是禁用,一个禁用状态的用户无法通过身份验证
	 *
	 * @return true表示可用,false表示禁用
	 */
	boolean isEnabled();

使用方法:

1.创建账户(Account)实体类

package com.jiazhong.office.model;

/**
 * @ClassName: Account
 * @Description: TODO 账户实体类
 * @Author: JiaShiXi
 * @Date: 2021/2/23 20:30
 * @Version: 1.0
 **/

public class Account {
    private Integer account_id;
    private String account_name;
    private String account_password;
    private Integer account_status;
    private Integer account_is_first;
    private String emp_id;

    public Integer getAccount_id() {
        return account_id;
    }

    public void setAccount_id(Integer account_id) {
        this.account_id = account_id;
    }

    public String getAccount_name() {
        return account_name;
    }

    public void setAccount_name(String account_name) {
        this.account_name = account_name;
    }

    public String getAccount_password() {
        return account_password;
    }

    public void setAccount_password(String account_password) {
        this.account_password = account_password;
    }

    public Integer getAccount_status() {
        return account_status;
    }

    public void setAccount_status(Integer account_status) {
        this.account_status = account_status;
    }

    public Integer getAccount_is_first() {
        return account_is_first;
    }

    public void setAccount_is_first(Integer account_is_first) {
        this.account_is_first = account_is_first;
    }

    public String getEmp_id() {
        return emp_id;
    }

    public void setEmp_id(String emp_id) {
        this.emp_id = emp_id;
    }
}

2.创建AccountDao下的通过账户名查询账户信息的方法(queryAccountByAccountName)

package com.jiazhong.office.dao.rbac;

import com.jiazhong.office.model.Account;
import org.apache.ibatis.annotations.Select;
import org.springframework.stereotype.Repository;

/**
 * @InterfaceName: AccountDao
 * @Description: TODO 账户持久层
 * @Author: JiaShiXi
 * @Date: 2021/2/23 20:29
 * @Version: 1.0
 **/
@Repository
public interface AccountDao {

    /**
     * 通过账户名查询账户信息
     * @param account_name 账户名
     * @return 账户对象
     */
    @Select("select * from tbl_account where account_name = #{account_name}")
    public Account queryAccountByAccountName(String account_name);
}

3.创建UserDetailsService的实现类UserDetailsServiceImpl

package com.jiazhong.office.service.rbac;

import com.jiazhong.office.dao.rbac.AccountDao;
import com.jiazhong.office.model.Account;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.ArrayList;
import java.util.List;

/**
 * @ClassName: UserDetailServiceImpl
 * @Description: TODO
 * @Author: JiaShiXi
 * @Date: 2021/2/23 20:26
 * @Version: 1.0
 **/

@Service("userDetailsService")
@Transactional
public class UserDetailsServiceImpl implements UserDetailsService {

    @Autowired
    private AccountDao accountDao;

    /**
     * 根据用户名获得用户信息
     *
     * @param username 用户名
     * @return 用户信息
     * @throws UsernameNotFoundException 用户名不存在异常
     */
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        Account account = accountDao.queryAccountByAccountName(username);
        if (account == null){
            throw new UsernameNotFoundException("账户名不存在");
        }

        //创建一个用于存储用户认证权限的集合
        List<GrantedAuthority>  grantedAuthorities = new ArrayList<>();
        grantedAuthorities.add(new SimpleGrantedAuthority("USER"));

        /**
         * 创建UserDetails实现类对象User,将认证信息传给User对象进行认证
         * 参数1:要认证用户的用户名
         * 参数2:要认证用户的密码
         * 参数3:要认证用户所拥有的权限集合
         */
        User userAuth = new User(account.getAccount_name(),account.getAccount_password(),grantedAuthorities);
        return userAuth;
    }
}

4.在SpringScurity的配置类的auth中设置用户登录服务层处理程序(userDetailsService)及加密器

@Autowired
private UserDetailsService userDetailsService;

@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    auth
        .userDetailsService(userDetailsService)//设置用户登录服务层处理程序
        .passwordEncoder(passwordEncoder()) //设置加密器
        ;

    //super.configure(auth);
}

/**
     * 返回带盐加密器对象
     * @return
     */
private PasswordEncoder passwordEncoder() {
    return new BCryptPasswordEncoder();
}

用数据库中正确的账户信息(admin、admin),显示登陆成功!

在这里插入图片描述

整个的流程如下:

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Jackson Xi

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值