1、什么是SpringSecurity
Spring Security 是 Spring 家族中的一个安全管理框架。相比与另外一个安全框架Shiro,它提供了更 丰富的功能,社区资源也比Shiro丰富。 一般来说中大型的项目都是使用SpringSecurity 来做安全框架。小项目有Shiro的比较多,因为相比与SpringSecurity,Shiro的上手更加的简单。 一般Web应用的需要进行认证和授权。
认证:验证当前访问系统的是不是本系统的用户,并且要确认具体是哪个用户
授权:经过认证后判断当前用户是否有权限进行某个操作
而认证和授权也是SpringSecurity作为安全框架的核心功能。
需要了解更深的请进入官网:Spring Security 中文文档 :: Spring Security Reference (springdoc.cn)
2、SpringSecurity的优势
Spring Security是一个功能强大的身份验证和访问控制框架,为Java应用程序提供了广泛的安全性功能。其优势包括:
- 综合性:Spring Security提供了综合的安全性解决方案,涵盖了身份验证、授权、攻击防护、会话管理等各个方面的功能,使得开发人员可以轻松地为应用程序添加安全性。
- 灵活性:Spring Security采用了模块化的设计,允许开发人员根据应用程序的需求选择性地配置和使用各种功能。它提供了丰富的配置选项和扩展点,可以轻松地集成到各种类型的应用程序中。
- 易用性:Spring Security提供了简单直观的API和配置方式,使得开发人员可以快速上手并使用其中的各种功能。它还提供了大量的文档和示例代码,帮助开发人员理解和使用框架。
- 安全性:Spring Security是一个经过广泛测试和验证的安全性框架,具有高度的稳定性和可靠性。它提供了多种安全性功能,如密码加密、CSRF防护、跨域请求防护等,可以有效地保护应用程序免受各种安全威胁。
Spring Security是一个功能强大、灵活易用、安全可靠的安全性框架,适用于各种类型的Java应用程序。
3、SpringBoot整合SpringSecurity
3.1、项目导入
我们需要再我们的SpringBoot项目当中引入SpringSecurity的依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
引入这个依赖以后我们服务器所有的资源全部都会被SpringSecurity安全框架所管理,它默认会有一个登录界面,用户名和密码会在我们启动项目的时候在IDEA的控制台看见。
但是在我们的项目当中,我们一般不会用它的默认登录界面,而是自己去手动写我们的登录逻辑。
我们的思路大概是这样,用户登陆成功以后,会颁发一个token也就是令牌,这个令牌就是一个用户的标识,以后的请求携带这个token来访问服务器的资源
3.2、工具类导入
因为这里不深入讲解JWT的使用 所以我就封装了一个生成token的工具,如果需要了解可以看小编另外一篇文章:http://t.csdnimg.cn/lmxdH
依赖
<!--jwt依赖-->
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.4.1</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
代码 类名JwtUtil
public class JwtUtil {
/**
* 生成token
* Algorithm.HMAC256():使用HS256生成token,密钥则是用户的密码
* withClaim():存入需要保存在token里的信息
* withExpiresAt():设置过期时间
* sign():生成token
* @param user
*/
private static final String SING = "BeautifulCodeEveryDay";
public static String getToken(SysUser user) {
//System.out.println(String.valueOf(user.getId()));
// 设置token过期时间
Calendar instance= Calendar.getInstance();
instance.add(Calendar.DATE,7);
String token="";
token= JWT.create()
.withClaim("id",String.valueOf(user.getId())) //设置载荷
.withClaim("username",user.getUsername())
.withExpiresAt(instance.getTime()) //设置令牌过期的时间
.sign(Algorithm.HMAC256(SING));
return token;
}
/**,
* 验证token 合法性
* verify():验证token是否正确
* @param token
*/
public static DecodedJWT verify(String token) {
return JWT.require(Algorithm.HMAC256(SING)).build().verify(token);
}
}
这里是使用户的id和用户名来生成token的
3.3、RABC数据库模型
RABC(Role-Based Access Control)是一种基于角色的访问控制模型,用于管理系统中的用户权限。在RABC模型中,权限通过角色进行管理,用户被分配到不同的角色,而角色则被赋予相应的权限。因为我们需要做权限检验,所以需要用到这个模型。
模型组成:
用户表 sys_user
菜单表sys_menu
角色表sys_role
用户角色表sys_user_role
角色菜单表sys_role_menu
模型图:
3.4、认证
认证(Authentication)是指确认用户或者实体的身份是否合法和有效的过程。认证通常用于确认用户是否是其所声称的身份,以便授予其对系统或资源的访问权限。
认证的目的是确保系统只允许合法用户访问资源,防止未经授权的用户获取敏感信息或者执行不当操作。通常,认证过程需要用户提供一些凭据来证明其身份,这些凭据可能包括密码、数字证书、生物特征等。系统会对用户提供的凭据进行验证,以确认用户的身份是否合法
SpringSecurity里面就内置了认证功能,它的原理其实就是一个过滤器链,内部包含了提供各种功能的过滤器。这里我们可以看看 其中的过滤器。
其中这几个是比较重要和常用的
UsernamePasswordAuthenticationFilter:负责处理我们在登陆页面填写了用户名密码后的登陆请 求。入门案例的认证工作主要有它负责。
ExceptionTranslationFilter:处理过滤器链中抛出的任何AccessDeniedException和
AuthenticationException ,FilterSecurityInterceptor:负责认证和权限校验的过滤器
在这里我们要自定义自己的登录功能,就要实现UserDetailsService并且重写loadUserByUsername
代码实现:类 UserDetailServiceImpl
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.hyh.ad.common.core.domain.login.LoginUser;
import com.hyh.ad.common.core.domain.model.SysUser;
import com.hyh.admin.sys.mapper.SysMenuMapper;
import com.hyh.admin.sys.mapper.SysUserMapper;
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.stereotype.Service;
import java.util.List;
import java.util.Objects;
@Service
public class UserDetailServiceImpl implements UserDetailsService {
@Autowired
private SysUserMapper sysUserMapper;
@Autowired
private SysMenuMapper sysMenuMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 根据用户名查询用户信息
LambdaQueryWrapper<SysUser> qw = new LambdaQueryWrapper<>();
qw.eq(SysUser::getUsername, username);
SysUser user = sysUserMapper.selectOne(qw);
if(Objects.isNull(user))
throw new UsernameNotFoundException("用户不存在");
// 查询权限信息
List<String> list = sysMenuMapper.selectAuthentication(user.getId());
//将权限存入到redis
return new LoginUser(user, list);
}
SysUserMapper 用户表的mapper 主要用于用户名和密码的校验
SysMenuMapper 菜单表的mapper 主要用于查询该用户具有的权限信息
因为UserDetailsService方法的返回值是UserDetails类型,所以需要定义一个类LoginUser,实现该接口,把用户 信息封装在其中。
import com.hyh.ad.common.core.domain.model.SysUser;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
@Data
@NoArgsConstructor
public class LoginUser implements UserDetails {
/*
* 用户信息
*/
private SysUser user;
public LoginUser(SysUser user, List<String> permission) {
this.user = user;
this.permissions = permission;
}
/*
* 权限信息
*/
private List<String> permissions;
private List<GrantedAuthority> authorities;
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
if(authorities!=null)
return authorities;
authorities = new ArrayList<>();
for (String permission : permissions) {
SimpleGrantedAuthority authority = new SimpleGrantedAuthority(permission);
authorities.add(authority);
}
return authorities;
}
@Override
public String getPassword() {
return user.getPassword();
}
@Override
public String getUsername() {
return user.getUsername();
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
接下我们需要自定义登陆接口,然后让SpringSecurity对这个接口放行,让用户访问这个接口的时候不用 登录也能访问。 在接口中我们通过AuthenticationManager的authenticate方法来进行用户认证,所以需要在SecurityConfig中配置把AuthenticationManager注入容器。 认证成功的话要生成一个token,放入响应中返回。
这里我就不写应用层接口了 就写业务逻辑层
@Override
public PageResult login(User user) {
UsernamePasswordAuthenticationToken authenticationToken
= new UsernamePasswordAuthenticationToken(user.getUsername(),
user.getPassword());
Authentication authenticate = authenticationManager.authenticate(authenticationToken);
if(Objects.isNull(authenticate.getPrincipal()))
throw new RuntimeException("用户名或密码错误");
LoginUser loginUser = (LoginUser) authenticate.getPrincipal();
User user1 = loginUser.getUser();
String token = JwtUtil.getToken(loginUser.getUser());
log.info("user1:{}", user1);
Map<String,String> map = new HashMap<>();
map.put("token",token);
map.put("id", String.valueOf(user1.getId()));
return new PageResult(HttpStatus.OK, map,"登录成功" );
}
这样的话我们就实现了用户登录的一个认证功能,大概就是登陆成功返还一个token,并且把用户的信息(权限之类)存放到 Authentication里面
我们需要自定义一个过滤器,这个过滤器会去获取请求头中的token,对token进行解析取出其中的
userid。 使用userid去获取对应的LoginUser对象。 然后封装Authentication对象存入SecurityContextHolder
类名:TokenAuthenticationFilter
import com.hyh.ad.common.core.domain.login.LoginUser;
import com.hyh.ad.common.core.domain.model.SysUser;
import com.hyh.admin.sys.mapper.SysMenuMapper;
import com.hyh.admin.sys.mapper.SysUserMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import static com.hyh.admin.tool.JwtUtil.verify;
/*
* tokek验证过滤器
* 验证token是否有效
*/
@Slf4j
@Component
public class TokenAuthenticationFilter extends OncePerRequestFilter {
@Autowired
private SysUserMapper sysUserMapper;
@Autowired
private SysMenuMapper sysMenuMapper;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
String token = request.getHeader("token");
if(token == null || token.isEmpty()){
// 放行请求
filterChain.doFilter(request, response);
return; // 结束方法
}
log.info("token:{}",token);
Long userId = null;
Map<String,String> map = new HashMap<>();
// 验证令牌
try{
userId = Long.valueOf(verify(token).getClaim("id").asString());
System.out.println(userId);
}catch (Exception e) {
e.printStackTrace();
map.put("msg","无效签名!");
map.put("state","false");
String json = new com.fasterxml.jackson.databind.ObjectMapper().writeValueAsString(map);
response.setContentType("application/json;charset=UTF-8");
response.getWriter().println(json);
return;
}
SysUser user = sysUserMapper.selectById(userId);
if(Objects.isNull(user)){
throw new RuntimeException("用户不存在");
}
LoginUser loginUser = new LoginUser(user,sysMenuMapper.selectAuthentication(userId));
//存入SecurityContextHolder
//获取权限信息封装到Authentication中
UsernamePasswordAuthenticationToken authenticationToken
= new UsernamePasswordAuthenticationToken(loginUser.getUser(),
null,
loginUser.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
filterChain.doFilter(request, response);
}
自定义认证和授权的异常处理器
类:AuthenticationEntryPointImpl
import cn.hutool.http.HttpStatus;
import com.alibaba.fastjson.JSON;
import com.hyh.ad.common.core.domain.AjaxResult;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@Component
public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
response.setContentType("application/json;charset=UTF-8");
response.setStatus(HttpStatus.HTTP_OK);
String Msg = "请求访问:"+ request.getRequestURI() + " ,认证失败,无法访问该资源...";
AjaxResult result = new AjaxResult(HttpStatus.HTTP_UNAUTHORIZED,Msg);
String json = JSON.toJSONString(result);
response.getWriter().print(json);
}
}
类: AccessDeniedHandlerImpl
import cn.hutool.http.HttpStatus;
import com.alibaba.fastjson.JSON;
import com.hyh.ad.common.core.domain.AjaxResult;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.stereotype.Component;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@Component
public class AccessDeniedHandlerImpl implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
response.setContentType("application/json;charset=UTF-8");
response.setStatus(HttpStatus.HTTP_OK);
String Msg = "请求访问:"+ request.getRequestURI() + " ,权限不足,无法访问该资源,请联系管理员...";
AjaxResult result = new AjaxResult(HttpStatus.HTTP_FORBIDDEN,Msg);
String json = JSON.toJSONString(result);
response.getWriter().print(json);
}
}
这里需要对SpringSecurity进行配置 放行登录接口 资源和把以上三个过滤器添加进去
import com.hyh.admin.exceptions.AccessDeniedHandlerImpl;
import com.hyh.admin.exceptions.AuthenticationEntryPointImpl;
import com.hyh.admin.filter.TokenAuthenticationFilter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
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.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import java.util.Arrays;
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true) // 启用方法级别的权限认证
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private TokenAuthenticationFilter authenticationFilter;
@Autowired
private AuthenticationEntryPointImpl authenticationEntryPoint;
@Autowired
private AccessDeniedHandlerImpl accessDeniedHandler;
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.cors()
.and()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
.antMatchers("/api/sysUser/login").permitAll()
.anyRequest().authenticated()
.and()
.addFilterBefore(authenticationFilter, UsernamePasswordAuthenticationFilter.class);
http.exceptionHandling()
.authenticationEntryPoint(authenticationEntryPoint)
.accessDeniedHandler(accessDeniedHandler);
}
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOrigins(Arrays.asList("*")); // 使用allowedOriginPatterns代替allowedOrigins
configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS")); // 允许的HTTP方法
configuration.setAllowedHeaders(Arrays.asList("*")); // 允许所有请求头
configuration.setAllowCredentials(true); // 允许发送凭据
configuration.setMaxAge(3600L); // 预检请求的有效期,单位秒
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration); // 对所有路径应用上面的CORS配置
return source;
}
这样我们服务器上所有的接口资源都被SpringSecurity保护起来了,并且我们制定了一些过滤器来处理一些异常情况。
这里我用postman来访问一下
我们可以看到没有token的情况下是无法访问到的,这里就是被 AuthenticationEntryPointImpl
异常处理器处理了并返还了相关信息
然后我们可以试下一下登录功能
看得见是返还了token的 我们需要复制这个token后面访问的资源都需要携带这个token
3.5、授权
授权(Authorization)是指确认用户、程序或者进程是否被允许访问资源或执行某些操作的过程。授权通常是在认证(Authentication)之后进行的,认证是确认用户身份的过程,而授权则是确定用户被允许进行的操作的过程。
授权的目的是确保系统中的资源受到适当的保护,只有经过授权的实体才能够访问资源或执行操作。授权通常基于用户的身份、角色、权限或者其他属性进行判断。例如,一个文件系统可以授权只有特定用户或者特定用户组才能够访问某个文件;一个网络应用程序可以授权只有特定用户或者特定角色才能够执行某个操作。
在授权过程中,通常会定义访问控制策略,这些策略确定了谁可以访问哪些资源以及以何种方式访问。这些策略可以在系统中的各个层面进行定义和管理,包括操作系统级别、应用程序级别以及网络服务级别等。
接下来我们写一个需要啊权限的接口 只有管理员账号才可以访问的资源
/**
* @description: 查询提交的资质认证
* @param:
* @return:
* @author
* @date: 2024/3/16 14:03
*/
@GetMapping("/selectQualification")
@PreAuthorize("hasAuthority('sys:manage:user')")
public PageResult selectQualification(@RequestParam("page") Integer page, @RequestParam("size") Integer size) {
return trainerService.selectQualification(page, size);
}
这些资源权限都在数据库里配置好了的。
我们用一个不是管理员账号的token去访问这个资源
我们可以看到是没有权限访问的
换一个管理员账号的token去访问
这样就能访问到了保护的资源了。
以上就是SpringBoot整合SpringSecurity+JWT的一个用户认证与授权的功能