Spring Security是基于Spring生态圈的,用于提供安全访问控制解决方案的框架。Spring Security的安 全管理有两个重要概念,分别是Authentication(认证)和Authorization(授权)。 为了方便Spring Boot项目的安全管理,Spring Boot对Spring Security安全框架进行了整合支持,并提 供了通用的自动化配置,从而实现了Spring Security安全框架中包含的多数安全管理功能。
Spring Security登录认证主要涉及两个重要的接口 UserDetailService和UserDetails接口。
UserDetailService接口主要定义了一个方法 loadUserByUsername(String username)用于完成用户信息的查 询,其中username就是登录时的登录名称,登录认证时,需要自定义一个实现类实现UserDetailService接 口,完成数据库查询,该接口返回UserDetail。
UserDetail主要用于封装认证成功时的用户信息,即UserDetailService返回的用户信息,可以用Spring
自己的User对象,但是最好是实现UserDetail接口,自定义用户对象。
1. 自定UserDetails类:当实体对象字段不满足时需要自定义UserDetails,一般都要自定义
UserDetails。
2. 自定义UserDetailsService类,主要用于从数据库查询用户信息。
3. 创建登录认证成功处理器,认证成功后需要返回JSON数据,菜单权限等。
4. 创建登录认证失败处理器,认证失败需要返回JSON数据,给前端判断。
5. 创建匿名用户访问无权限资源时处理器,匿名用户访问时,需要提示JSON。
6. 创建认证过的用户访问无权限资源时的处理器,无权限访问时,需要提示JSON。
7. 配置Spring Security配置类,把上面自定义的处理器交给Spring Security。
在pom.xml文件中添加Spring Security核心依赖,代码如下所
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
当实体对象字段不满足时Spring Security认证时,需要自定义UserDetails。
1. 将User类实现UserDetails接口
2. 将原有的isAccountNonExpired、isAccountNonLocked、isCredentialsNonExpired和isEnabled属性修 改成boolean类型,同时添加authorities属性。
@Data
@TableName("sys_user")
public class User implements Serializable, UserDetails {
//省略原有的属性......
/**
* 帐户是否过期(1 未过期,0已过期)
*/
private boolean isAccountNonExpired = true;
/**
* 帐户是否被锁定(1 未过期,0已过期)
*/
private boolean isAccountNonLocked = true;
/**
* 密码是否过期(1 未过期,0已过期)
*/
private boolean isCredentialsNonExpired = true;
/**
* 帐户是否可用(1 可用,0 删除用户)
*/
private boolean isEnabled = true;
/**
* 权限列表
*/
@TableField(exist = false)
Collection<? extends GrantedAuthority> authorities;
public interface UserService extends IService<User> {
/**
* 根据用户名查询用户信息
* @param userName
* @return
*/
User findUserByUserName(String userName);
}
package com.manong.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.manong.entity.User;
import com.manong.dao.UserMapper;
import com.manong.service.UserService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
/**
* <p>
* 服务实现类
* </p>
*
* @author lemon
* @since 2022-12-06
*/
@Service
@Transactional
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {
@Override
public User findUserByUserName(String username) {
//创建条件构造器对象
QueryWrapper queryWrapper=new QueryWrapper();
queryWrapper.eq("username",username);
//执行查询
return baseMapper.selectOne(queryWrapper);
}
}
package com.manong.config.security.service;
import com.manong.entity.Permission;
import com.manong.entity.User;
import com.manong.service.PermissionService;
import com.manong.service.UserService;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;
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.Component;
import javax.annotation.Resource;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
/*
* 用户认证处理器类
* */
@Component
public class CustomerUserDetailService implements UserDetailsService {
@Resource
private UserService userService;
@Resource
private PermissionService permissionService;
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException{
//根据对象查找用户信息
User user = userService.findUserByUserName(username);
//判断对象是否为空
if(user==null){
throw new UsernameNotFoundException("用户的账号密码错误");
}
//查询当前登录用户拥有权限列表
List<Permission> permissionList = permissionService.findPermissionListByUserId(user.getId());
//获取对应的权限编码
List<String> codeList = permissionList.stream()
.filter(Objects::nonNull)
.map(item -> item.getCode())
.filter(Objects::nonNull)
.collect(Collectors.toList());
//将权限编码转换成数据
String [] strings=codeList.toArray(new String[codeList.size()]);
//设置权限列表
List<GrantedAuthority> authorityList = AuthorityUtils.createAuthorityList(strings);
//将权限列表设置给User
user.setAuthorities(authorityList);
//设置该用户拥有的菜单
user.setPermissionList(permissionList);
//查询成功
return user;
}
}
4.1.成功
package com.manong.config.security.handler;
import com.alibaba.fastjson.serializer.SerializerFeature;
import com.manong.entity.User;
import com.manong.utils.JwtUtils;
import com.manong.utils.LoginResult;
import com.manong.utils.ResultCode;
import io.jsonwebtoken.Jwts;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.stereotype.Component;
import sun.net.www.protocol.http.AuthenticationHeader;
import javax.annotation.Resource;
import javax.servlet.ServletException;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import com.alibaba.fastjson.JSON;
/*
* 登录认证成功处理器类
* */
@Component
public class LoginSuccessHandler implements AuthenticationSuccessHandler {
@Resource
private JwtUtils jwtUtils;
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
//设置相应编码格式
response.setContentType("application/json;charset-utf-8");
//获取当前登录用户的信息
User user = (User) authentication.getPrincipal();
//创建token对象
String token = jwtUtils.generateToken(user);
//设置token的秘钥和过期时间
long expireTime = Jwts.parser()
.setSigningKey(jwtUtils.getSecret())
.parseClaimsJws(token.replace("jwt_", ""))
.getBody().getExpiration().getTime();//设置过期时间
//创建LOgin登录对象
LoginResult loginResult=new LoginResult(user.getId(), ResultCode.SUCCESS,token,expireTime);
//将对象转换成json格式
//消除循环引用
String result = JSON.toJSONString(loginResult, SerializerFeature.DisableCircularReferenceDetect);
//获取输出流
ServletOutputStream outputStream = response.getOutputStream();
outputStream.write(result.getBytes(StandardCharsets.UTF_8));
outputStream.flush();
outputStream.close();
}
}
package com.manong.config.security.handler;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.serializer.SerializerFeature;
import com.baomidou.mybatisplus.extension.api.R;
import com.manong.entity.User;
import com.manong.utils.Result;
import com.manong.utils.ResultCode;
import org.springframework.security.authentication.*;
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.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
@Component
public class LoginFailureHandler implements AuthenticationFailureHandler {
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
//设置相应编码格式
response.setContentType("application/json;charset-utf-8");
//获取输出流
ServletOutputStream outputStream = response.getOutputStream();
//定义变量,保存异常信息
String message=null;
//判断异常类型
if(exception instanceof AccountExpiredException){
message="账户过期失败";
}
else if(exception instanceof BadCredentialsException){
message="用户名的账号密码错误,登录失败";
}
else if(exception instanceof CredentialsExpiredException){
message="密码过期,登录失败";
}
else if(exception instanceof DisabledException){
message="账号过期,登录失败";
}
else if(exception instanceof LockedException){
message="账号被上锁,登录失败";
}
else if(exception instanceof InternalAuthenticationServiceException){
message="用户不存在";
}
else {
message="登录失败";
}
//将结果转换为Json格式
String result = JSON.toJSONString(Result.error().code(ResultCode.ERROR).message(message));
//将结果保存到输出中
outputStream.write(result.getBytes(StandardCharsets.UTF_8));
outputStream.flush();
outputStream.close();
}
}
package com.manong.config.security.handler;
import com.alibaba.fastjson.serializer.SerializerFeature;
import com.manong.entity.User;
import com.manong.utils.Result;
import com.manong.utils.ResultCode;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.stereotype.Component;
import sun.net.www.protocol.http.AuthenticationHeader;
import javax.servlet.ServletException;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import com.alibaba.fastjson.JSON;
/*
* 匿名访问无权限资源处理器
* */
@Component
public class AnonymousAuthenticationHandler implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
response.setContentType("application/json;charset-utf-8");
//获取输出流
ServletOutputStream outputStream = response.getOutputStream();
//将对象转换成json格式
//消除循环引用
String result = JSON.toJSONString(Result.error().code(ResultCode.NO_AUTH).message("匿名用户无权限访问"), SerializerFeature.DisableCircularReferenceDetect);
//获取输出流
outputStream.write(result.getBytes(StandardCharsets.UTF_8));
outputStream.flush();
outputStream.close();
}
}
package com.manong.config.security.handler;
import com.alibaba.fastjson.serializer.SerializerFeature;
import com.manong.entity.User;
import com.manong.utils.Result;
import com.manong.utils.ResultCode;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.stereotype.Component;
import sun.net.www.protocol.http.AuthenticationHeader;
import javax.servlet.ServletException;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import com.alibaba.fastjson.JSON;
/*
* 认证用户访问无权限资源处理器
* */
@Component
public class CustomerAccessDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
response.setContentType("application/json;charset-utf-8");
ServletOutputStream outputStream = response.getOutputStream();
//将对象转换成json格式
//消除循环引用
String result = JSON.toJSONString(Result.error().code(ResultCode.NO_AUTH).message("用户无权限访问,请联系教务处"), SerializerFeature.DisableCircularReferenceDetect);
//获取输出流
outputStream.write(result.getBytes(StandardCharsets.UTF_8));
outputStream.flush();
outputStream.close();
}
}
package com.manong.config.security.service;
import com.manong.config.security.handler.AnonymousAuthenticationHandler;
import com.manong.config.security.handler.CustomerAccessDeniedHandler;
import com.manong.config.security.handler.LoginFailureHandler;
import com.manong.config.security.handler.LoginSuccessHandler;
import org.springframework.context.annotation.Bean;
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.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
@Component
@EnableWebSecurity
public class SpringSecurityConfig extends WebSecurityConfigurerAdapter {
@Resource
private LoginSuccessHandler loginSuccessHandler;
@Resource
private LoginFailureHandler loginFailureHandler;
@Resource
private CustomerAccessDeniedHandler customerAccessDeniedHandler;
@Resource
private AnonymousAuthenticationHandler anonymousAuthenticationHandler;
@Resource
private CustomerUserDetailService customerUserDetailService;
//注入加密类
@Bean
public BCryptPasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
//处理登录认证
@Override
protected void configure(HttpSecurity http) throws Exception {
//登录过程处理
http.formLogin() //表单登录
.loginProcessingUrl("/api/user/login") //登录请求url地址
.successHandler(loginSuccessHandler) //认证成功
.failureHandler(loginFailureHandler) //认证失败
.and()
.csrf().disable()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS) //不创建Session
.and().authorizeRequests() //设置需要拦截的请求
.antMatchers("/api/user/login").permitAll()//登录放行
.anyRequest().authenticated() //其他请求一律拦截
.and()
.exceptionHandling()
.authenticationEntryPoint(anonymousAuthenticationHandler) //匿名无权限类
.accessDeniedHandler(customerAccessDeniedHandler) //认证用户无权限
.and()
.cors();//支持跨域
}
//认证配置处理器
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(customerUserDetailService)
.passwordEncoder(this.passwordEncoder());//密码加密
}
}