前言:Spring Security是spring提供的一个安全框架,提供了登录认证、密码保护、自动登录等。这是我目前学习到的功能,当然它的强大之处远不止这些了。Spring Security处理登录功能时使用的
form表单,底层获取参数使用的request.getParamter的形式获取的。但是前后端分离模式使用的是异步请求,所以在前后端分离模式下会出现很多问题。以下记录了我出现过的问题。
这里后端使用的是springboot+spring security,使用插件fastjson以及lombok
前端使用Vue+axios+ElementUi(主要为了操作简单)
编写Security配置类,继承WebSecurityConfigurerAdapter类,重写两个重要的方法configure(HttpSecurity http)和configure(AuthenticationManagerBuilder auth);参数为http的方法主要配置拦截请求后的操作,比如登录页面,登陆了成功操作,失败操作,或者跨域请求登录问题。参数为auth主要是配置登录认证时使用自定义认证方法。这里需要注意一下几点:
必须配置PasswordEncoder @Bean对象, Security的底层是强制对密码进行编码的,如果没有配置,会报错PasswordEncoder为null。
Security需要开启跨域访问,代码在参数为http的方法编写
Security内部有自带的login页面,当未登录时会默认跳转改页面,如果是前后端分离模式,则会出现302的错误。需要将跳转重新设置,前后端分离情况下不需要返回页面,只需要返回未登录信息,所以应该写一个mapping方法,返回未登录信息,如下:
@RestController
public class LoginController {
@GetMapping("/unLogin")
public String unLogin(){
return "未登录";
}
}
并在配置方法中配置登录页跳转即可: http.loginPage("/unLogin")
package com.example.securitydemo.config;
import com.alibaba.fastjson.JSON;
import com.example.securitydemo.service.MyUserDetailsService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.DisabledException;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.rememberme.JdbcTokenRepositoryImpl;
import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository;
import javax.servlet.http.HttpServletResponse;
import javax.sql.DataSource;
import java.io.PrintWriter;
import java.util.HashMap;
import java.util.Map;
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
//自定义Sevice实现UserDetails
@Autowired
private MyUserDetailsService myUserDetailsService;
//前提配置文件中中文件置配置数据源
@Autowired
private DataSource dataSource;
/**
* 实现自动登录功能,配置数据源
*/
@Bean
public PersistentTokenRepository persistentTokenRepository(){
//使用JDBC
JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl();
jdbcTokenRepository.setDataSource(dataSource);
//创建数据库,只能执行一次,第二次之后需要注掉,否则报错数据库已存在
// jdbcTokenRepository.setCreateTableOnStartup(true);
return jdbcTokenRepository;
}
/**
*
* 如果需要实现自己的编码格式,可以实现PasswordEncoder接口,重写encode()方法实现编码
* 设置编码格式,必须设置,否则报错
*
* @return
*/
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
/**
* @param auth
* @throws Exception
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//注入自己的userDetailsService
auth.userDetailsService(myUserDetailsService)
//设置密码编码格式
.passwordEncoder(passwordEncoder());
}
/**
* 拦截请求后的权限设置
*
* @param http
* @throws Exception
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/test1").authenticated()//设置必须持有凭证才可以访问路径
.antMatchers("/test2").permitAll()//设置无需凭证访问路径
.and().formLogin() //使用自带的登录
//登录页表示未登录的时候跳转的路径
.loginPage("/unLogin")
.loginProcessingUrl("/login")//设置登录请求路径
//登录失败,返回json
.failureHandler((request, response, ex) -> {
response.setContentType("application/json;charset=utf-8");
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
PrintWriter out = response.getWriter();
Map<String, Object> map = new HashMap();
map.put("code", 401);
if (ex instanceof UsernameNotFoundException || ex instanceof BadCredentialsException) {
map.put("message", "用户名或密码错误");
} else if (ex instanceof DisabledException) {
map.put("message", "账户被禁用");
} else {
map.put("message", "登录失败!");
}
out.println(JSON.toJSONString(map));
out.flush();
out.close();
})
//登录成功,返回json
.successHandler((request, response, authentication) -> {
Map<String, Object> map = new HashMap();
map.put("code", 200);
map.put("message", "登录成功");
map.put("data", authentication);
response.setContentType("application/json;charset=utf-8");
PrintWriter out = response.getWriter();
out.println(JSON.toJSONString(map));
out.flush();
out.close();
})
.and()
.exceptionHandling()
//没有权限,返回json
.accessDeniedHandler((request, response, ex) -> {
response.setContentType("application/json;charset=utf-8");
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
PrintWriter out = response.getWriter();
Map<String, Object> map = new HashMap();
map.put("code", 403);
map.put("message", "权限不足");
out.println(JSON.toJSONString(map));
out.flush();
out.close();
})
//自动登录(记住我功能)
.and()
.rememberMe().tokenRepository(persistentTokenRepository())
.rememberMeParameter("rememberMe")//参数名称
.userDetailsService(myUserDetailsService)//使用自定义service操作数据库
.tokenValiditySeconds(60)//已秒为单位
.and()
.logout()
//退出登录url
.logoutUrl("/logout")
//退出成功,返回json
.logoutSuccessHandler((request, response, authentication) -> {
Map<String, Object> map = new HashMap();
map.put("code", 200);
map.put("message", "退出成功");
map.put("data", authentication);
response.setContentType("application/json;charset=utf-8");
PrintWriter out = response.getWriter();
out.println(JSON.toJSONString(map));
out.flush();
out.close();
})
.permitAll()
//开启跨域访问
.and().cors()
//开启模拟请求,比如API POST测试工具的测试,不开启时,API POST为报403错误
.and().csrf().disable()
//未登录返回状态设置
.exceptionHandling().authenticationEntryPoint((request, response, e) ->
//没有访问权限
response.setStatus(HttpStatus.UNAUTHORIZED.value()));;
}
}
package com.example.securitydemo.service;
import com.example.securitydemo.entity.User;
import com.example.securitydemo.mapper.UserMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;
@Service
public class MyUserDetailsService implements UserDetailsService {
@Autowired
private UserMapper userMapper;
/**
* 重写loadUserByUsername方法,实现登录验证
*/
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userMapper.selectByUserName(username);
//如果用户名不存在
if (user == null) {
throw new UsernameNotFoundException("用户名不存在");
}
user.setPassword(new BCryptPasswordEncoder().encode(user.getPassword()));
return user;
}
}
package com.example.securitydemo.entity;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.Collection;
import java.util.List;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class User implements UserDetails {
private String username;
private String password;
/**
* 访问权限的角色,这里可以定义用户的访问权限
* @return
*/
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
//访问角色,这里暂时没有用。据网上说这里设置为null会显示没有访问权限登录不进去。
// 但是我没有出现过这个问题,这里先简单的设置一下,不过没有什么用处
List<GrantedAuthority> authorities = AuthorityUtils.commaSeparatedStringToAuthorityList("role");
return authorities;
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
<div style="width: 300px;height: 100px;">
<el-form ref="form" :model="form" label-width="80px">
<el-form-item label="账号">
<el-input v-model="form.username">el-input>
el-form-item>
<el-form-item label="密码">
<el-input v-model="form.password">el-input>
el-form-item>
<el-form-item>
<el-checkbox v-model="form.rememberMe">记住我el-checkbox>
el-form-item>
<el-form-item>
<el-button type="primary" @click="onSubmit">登录el-button>
<el-button>取消el-button>
el-form-item>
el-form>
div>
<template>
<div id="app">
<div style="width: 300px;height: 100px;">
<el-form ref="form" :model="form" label-width="80px">
<el-form-item label="账号">
<el-input v-model="form.username">el-input>
el-form-item>
<el-form-item label="密码">
<el-input v-model="form.password">el-input>
el-form-item>
<el-form-item>
<el-checkbox v-model="form.rememberMe">记住我el-checkbox>
el-form-item>
<el-form-item>
<el-button type="primary" @click="onSubmit">登录el-button>
<el-button>取消el-button>
el-form-item>
el-form>
div>
div>
template>
<script>
export default {
data() {
return {
form: {}
}
},
methods: {
onSubmit() {
let qs = require('qs');
//使用qs将对象转换为form表单
this.$axios.post("/login",qs.stringify(this.form))
.then((response) => {
console.log(response)
}).catch((err) => {
console.log(err)
})
}
}
}
script>
<style>
#app {}
style>
以上就是我遇到的问题。前端的额外处理这里就不介绍了,可以在axios的拦截器功能中拦截请求,如果登录失败的*跳转登录页即可。记住我的功能可以访问后端获取用户信息,这样当用户不能访问信息时,表示账户登录信息失效。非常完美的框架