项目日记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),显示登陆成功!
整个的流程如下: