本案例是使用SpringBoot2.7.6+security+MyBatis-plus+Vue2+axios实现
一、security简介
Spring Security是一个功能强大且高度可定制的身份验证和访问控制框架,专为Java应用程序设计。
(1)基本功能
- 身份验证(Authentication):确定用户身份的过程。在Spring Security中,这包括确认用户是谁,并验证用户提供的凭证(如用户名和密码)是否正确。
- 授权(Authorization):确定用户是否有权进行某个操作的过程。根据用户的身份和角色,Spring Security授予用户访问应用程序资源的权限。
(2)主要特点
- 全面性:Spring Security提供了全面的安全性解决方案,包括认证、授权、攻击防范(如XSS、CSRF等)和会话管理等功能。
- 可扩展性:提供了一系列可扩展的模块,可以根据具体需求进行选择和配置,如不同的身份验证方式、授权方式、密码编码器等。
- 易用性:提供了快捷配置选项和基于注解的安全控制方式,使开发人员能够更轻松地实现认证和授权等功能。
- 社区支持:作为Spring生态系统的一部分,Spring Security得到了广泛的社区支持和更新维护。
(3)工作原理
Spring Security通过一系列过滤器(Filter)来保护应用程序。这些过滤器按照特定的顺序组成过滤器链,每个过滤器都有特定的责任,如身份验证、授权、防止CSRF攻击等。当用户发起请求时,请求会经过过滤器链的处理,并在处理过程中进行安全验证和授权。
(4)核心组件
- SecurityContextHolder:用于存储安全上下文(SecurityContext)的信息,包括当前用户的身份信息、所拥有的权限等。
- AuthenticationManager:负责处理用户的身份验证,接收用户提交的凭据,并使用已配置的身份验证提供程序(AuthenticationProvider)进行验证。
- AuthenticationProvider:实际执行身份验证的组件,从用户存储源(如数据库、LDAP等)中获取用户信息,并进行密码比对或其他验证方式。
- UserDetailsService:用于加载UserDetails对象的接口,通常从数据库或LDAP服务器中获取用户信息。
- AccessDecisionManager:用于在授权过程中进行访问决策,根据用户的认证信息、请求的URL和配置的权限规则,判断用户是否有权访问资源。
二、依赖项
主要使用到Security、mysql和gson等依赖:
<!-- security -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!--MySQL驱动-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!-- mybatis-plus -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.4.2</version>
</dependency>
<!-- Gson: Java to Json conversion -->
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.8.9</version>
</dependency>
其中gson是用于吧java对象转成json格式,便于响应数据。
三、配置和自定义处理器
(1)关于security的配置
在config层中的security的配置主要有SecurityConfig核心配置类和CorsConfig访问配置类。
SecurityConfig核心配置类:
在yaml配置文件中,可以自定义前端登录页面:
spring:
security:
# 前后端分离时自定义的security的登录页面
loginPage: http://127.0.0.1:8081/#/login
import com.security.demo.handel.CustomAccessDeniedHandler;
import com.security.demo.handel.CustomAuthenticationFailureHandler;
import com.security.demo.handel.CustomAuthenticationSuccessHandler;
import com.security.demo.service.Impl.UserServiceImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpStatus;
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.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.authentication.logout.HttpStatusReturningLogoutSuccessHandler;
/**
* 配置security
*/
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Value("${spring.security.loginPage}")
private String loginPage;
@Autowired
private CustomAuthenticationSuccessHandler customAuthenticationSuccessHandler;
@Autowired
private CustomAuthenticationFailureHandler customAuthenticationFailureHandler;
@Autowired
private CustomAccessDeniedHandler customAccessDeniedHandler;
@Autowired
private UserServiceImpl userServiceImpl;
@Override
protected void configure(HttpSecurity http) throws Exception {
// 添加请求授权规则
http.authorizeRequests()
// 首页所有人都可以访问
.antMatchers("/public/**").permitAll()
// user下的所有请求,user角色权限才能访问
.antMatchers("/user/**").hasRole("USER")
// admin下的所有请求,ADMIN角色权限才能访问
.antMatchers("/admin/**").hasRole("ADMIN")
// 其他任何请求都要验证身份
.anyRequest().authenticated();
// 开启登录页面,即没有权限的话跳转到登录页面
http.formLogin()
// 登录页面
.loginPage(loginPage)
// 用户名的name
.usernameParameter("user")
// 密码的name
.passwordParameter("pwd")
// 处理登录的Controller
.loginProcessingUrl("/login")
// 验证成功处理器
.successHandler(customAuthenticationSuccessHandler)
// 验证失败处理器
.failureHandler(customAuthenticationFailureHandler);
http.csrf().disable();
// 开启记住我功能,默认保存两周
http.rememberMe()
// name属性
.rememberMeParameter("remember");
// 开启退出登录功能
http.logout()
.logoutUrl("/logout")
.logoutSuccessHandler(new HttpStatusReturningLogoutSuccessHandler(HttpStatus.OK)); // 自定义登出成功后的处理
// 跨域
http.cors();
// 权限不足处理器
http.exceptionHandling().accessDeniedHandler(customAccessDeniedHandler);
}
// 认证规则
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// 在新版本的SpringSecurity中新增了许多加密方法,这里使用的是BCrypt
auth.userDetailsService(userServiceImpl).passwordEncoder(new BCryptPasswordEncoder());
}
}
CorsConfig访问配置类:
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
* 配置security的跨域访问
*/
@Configuration
public class CorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOriginPatterns("*")
.allowedMethods("GET", "HEAD", "POST", "PUT", "DELETE", "OPTIONS")
.allowCredentials(true)
.exposedHeaders("Access-Control-Allow-Headers",
"Access-Control-Allow-Methods",
"Access-Control-Allow-Origin",
"Access-Control-Max-Age",
"X-Frame-Options")
.maxAge(3600)
.allowedHeaders("*");
}
}
(2)处理器Handler
业务开发时主要是自定义登录认证成功、登录认证失败和权限不足的处理器
登录认证成功后的处理器CustomAuthenticationSuccessHandler,主要判断验证码是否正确和响应返回R统一返回类。CustomAuthenticationSuccessHandler类:
import com.google.gson.Gson;
import com.security.demo.vo.R;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.stereotype.Component;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* 自定义的security的认证成功处理器
*/
@Component("customAuthenticationSuccessHandler")
public class CustomAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication)
throws IOException, ServletException {
R result = R.ok().setMessage("登录成功。");
response.setContentType("application/json;charset=UTF-8");
// 校验验证码
String code = (String) request.getSession().getAttribute("code");
String codeInput = request.getParameter("codeInput");
if(code==null || codeInput==null) {
result.setCode(501).setMessage("验证码为空");
}else {
if (!code.equalsIgnoreCase(codeInput)) {
result.setCode(501).setMessage("验证码错误");
}
}
Gson gson = new Gson();
response.getWriter().write(gson.toJson(result));
}
}
登录验证失败处理器CustomAuthenticationFailureHandler,主要用于响应返回R统一返回类。CustomAuthenticationFailureHandler类:
import com.google.gson.Gson;
import com.security.demo.vo.R;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.stereotype.Component;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* 自定义的security的认证失败处理器
*/
@Component
public class CustomAuthenticationFailureHandler implements AuthenticationFailureHandler {
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception)
throws IOException, ServletException {
//以返回JSON数据为例
R result = R.error().setMessage("用户名或密码错误。");
response.setContentType("application/json;charset=UTF-8");
Gson gson = new Gson();
response.getWriter().write(gson.toJson(result));
}
}
用户访问权限不足处理器CustomAccessDeniedHandler,主要返回权限不足信息。CustomAccessDeniedHandler类:
import com.google.gson.Gson;
import com.security.demo.vo.R;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* 用户权限不足被拒绝处理器
*/
@Component
public class CustomAccessDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException)
throws IOException {
R result = R.error().setMessage("权限不足");
response.setContentType("application/json;charset=UTF-8");
Gson gson = new Gson();
response.getWriter().write(gson.toJson(result));
}
}
(3)重写登录验证逻辑
在配置类中可以自定义指定的登录验证逻辑类,一般是写在service的实现类中,要求是该登录验证逻辑类必须实现UserDetailsService接口的loadUserByUsername方法,才可以在configure的auth.userDetailsService(自定义登录验证逻辑类)指定。这里我直接在UserServiceImpl类中实现:
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.security.demo.entity.User;
import com.security.demo.mapper.UserMapper;
import com.security.demo.service.UserService;
import com.security.demo.vo.R;
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.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;
import java.util.ArrayList;
import java.util.List;
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService, UserDetailsService {
@Autowired
private UserMapper userMapper;
/**
* 重写UserDetailsService的loadUserByUsername方法
* 用于登录验证和角色授权
*/
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = this.getUserByName(username);
if (user == null) {
throw new UsernameNotFoundException("用户名为null");
}
// 这里需要将User转换为UserDetails,并设置角色的GrantedAuthority集合
List<GrantedAuthority> authorities = new ArrayList<>();
authorities.add(new SimpleGrantedAuthority("ROLE_"+user.getRole()));
// 如果是admin角色,就多添加USER权限
if(user.getRole().equals("ADMIN")){
authorities.add(new SimpleGrantedAuthority("ROLE_USER"));
}
BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
String password = encoder.encode(user.getPassword());
return new org.springframework.security.core.userdetails.User(user.getName(), password, authorities);
}
/**
* 分页条件查询用户名
*/
@Override
public R getUserList(String name, int current, int size) {
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
Page<User> page = new Page<>(current, size);
if(name!=null && !name.isEmpty()){
queryWrapper.like("name",name);
}
queryWrapper.orderByDesc("id"); // 指定根据id倒序
Page<User> userPage = userMapper.selectPage(page, queryWrapper);
return R.ok().data("userPage", userPage);
}
/**
* 新增用户
*/
@Override
public R addUser(User user) {
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("name", user.getName());
User user1 = userMapper.selectOne(queryWrapper);
if (user1 != null) {
return R.error().setMessage("该用户名已存在");
}
userMapper.insert(user);
return R.ok().setMessage("新增成功!");
}
/**
* 更新用户
*/
@Override
public R updateUser(User user) {
// 根据用户名查询是否重复
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("name", user.getName());
User user1 = userMapper.selectOne(queryWrapper);
if (user1 != null && user1.getId() != user.getId()) {
return R.error().setMessage("该用户名已存在");
}
// 不重复就根据id来修改用户信息
QueryWrapper<User> queryWrapper1 = new QueryWrapper<>();
queryWrapper1.eq("id", user.getId());
userMapper.update(user, queryWrapper1);
return R.ok().setMessage("更新成功!");
}
/**
* 删除用户
*/
@Override
public R deleteUser(String name) {
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("name", name);
User user = userMapper.selectOne(queryWrapper);
if (user == null) {
return R.error().setMessage("没有该用户");
}
userMapper.deleteById(user.getId());
return R.ok().setMessage("删除成功");
}
/**
* 根据用户名查询用户信息
*/
@Override
public User getUserByName(String name) {
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("name", name);
return userMapper.selectOne(queryWrapper);
}
}
四、postman测试登录接口
使用postman测试登录login接口时要注意,由于security的限制,需要模仿表单提交,而不是常规的json数据,获取验证码后,使用x-www-form-urlencoded格式。如下:
五、前端axios注意事项
获取验证码后,使用x-www-form-urlencoded格式的post请求如下,参数是json数据。
// 登录
login(data){
return axios({
method: 'post',
url: '/login',
data: data,
// 模仿表单提交
transformRequest: [
function (data) {
let ret = ''
for (let it in data) {
ret +=
encodeURIComponent(it) +
'=' +
encodeURIComponent(data[it]) +
'&'
}
return ret
}
],
headers:{
'Content-Type': 'application/x-www-form-urlencoded'
}
})
},
六、案例完整源码
以上是security的核心代码和调用踩坑点,完整代码请转到码云仓库查看:
=========================================================================
Gitee仓库源码:https://gitee.com/BuLiangShuai01033/security-demo
作者:不凉帅 https://blog.csdn.net/yueyue763184
=========================================================================