目录
前提引入:
随着科技的完善,现在几乎所有的网站以及软件都需要进行授权认证,使之更加的安全可靠
认证:验证当前访问系统的是不是本系统的用户,并且要确认具体是哪个用户
授权:经过认证后判断当前用户是否有权限进行某个操作
定义:Spring Security 是一个功能强大且灵活的身份验证和授权框架,用于保护基于 Spring 的应用程序;它提供了一套综合的安全性解决方案,可以用于 Web 应用程序、REST API、微服务等各种应用场景
Spring Security 的主要功能和作用如下:
身份验证(Authentication):Spring Security 提供了多种身份验证方式,包括基于表单、HTTP 基本认证、LDAP、OAuth2 等。它可以集成到应用程序中,通过验证用户提供的凭据(如用户名和密码)来验证用户身份
授权(Authorization):Spring Security 支持基于角色和权限的授权机制。它允许您定义细粒度的访问控制规则,例如指定哪些用户具有访问某些受保护资源的权限
攻击防护(Attack Protection):Spring Security 提供了一系列的防护机制来应对常见的安全攻击,例如跨站点请求伪造(CSRF)攻击、会话固定攻击、点击劫持等。它通过配置合适的安全措施来保护应用程序免受这些攻击的风险
集成第三方认证系统:Spring Security 可以与其他身份认证系统(如LDAP、OAuth2)进行集成,以便使用这些系统中已有的用户凭据进行身份验证
定制化和扩展性:Spring Security 提供了丰富的配置选项和可插拔的拦截器机制,使开发人员可以根据应用程序的需求进行灵活的定制和扩展
审计和日志记录:Spring Security 可以记录关于身份验证和授权过程的审计日志,为应用程序的安全性监控和追踪提供支持
登录校验流程图解:
SpringSecurity 安全框架其实是多个过滤器(过滤器链)所组成的,内部包含了各种功能
SpringSecurity 中过滤器详解图:
UsernamePasswordAuthenticationFilter:负责处理我们在登陆页面填写了用户名密码后的登陆请求。入门案例的认证工作主要由它负责
ExceptionTranslationFilter:处理过滤器链中抛出的任何 AccessDeniedException 和AuthenticationException 异常(即 用户授权异常 和 用户认证异常)
FilterSecurityInterceptor:负责权限校验的过滤器
认证流程详解图:
Authentication接口: 它的实现类,表示当前访问系统的用户,封装了用户相关信息
AuthenticationManager接口:定义了认证Authentication的方法
UserDetailsService接口:加载用户特定数据的核心接口;里面定义了一个根据用户名查询用户信息的方法
UserDetails接口:提供核心用户信息;通过 UserDetailsService 根据用户名获取处理的用户信息要封装成 UserDetails 对象返回;然后将这些信息封装到 Authentication 对象中
这里设定一个 UserLogin 类,来继承 UserDetails 接口,用来表示登录用户的信息,便于之后的调用(User 为用户类)
- @Data
- @NoArgsConstructor
- @AllArgsConstructor
- public class LoginUser implements UserDetails {
-
- private User user;
-
-
- /**
- * 获取权限信息
- */
- @Override
- public Collection extends GrantedAuthority> getAuthorities() {
-
- return null;
- }
-
- @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 将其放行,使用户在不需要登录的时候也能够访问;同时,可以设置过滤器的前后位置(这里将JWT认证过滤器放在用户登录过滤器之前)
代码如下:
这里是用户登录验证的接口
- /**
- * 【用户的验证】
- */
- @Service
- public class LoginServiceImpl implements LoginService {
-
- @Resource
- private AuthenticationManager authenticationManager;
-
- @Resource
- private RedisCache redisCache;
-
- @Override
- public ResponseResult login(User user) {
-
- //1.使用 authenticate 方法进行用户的验证
- UsernamePasswordAuthenticationToken authenticationToken
- = new UsernamePasswordAuthenticationToken(user.getUserName(),user.getPassword());
-
- Authentication authenticateResult = authenticationManager.authenticate(authenticationToken);
-
- //2.如果认证没通过,则进行提示
- if(Objects.isNull(authenticateResult)){
-
- throw new RuntimeException("用户名或密码错误!");
- }
-
- //3.如果认证通过,使用 userId 生成一个 jwt
- LoginUser loginUser = (LoginUser) authenticateResult.getPrincipal(); //当前登录用户信息
- Long userId = loginUser.getUser().getId();
- String jwt = JwtUtil.createJWT(userId.toString()); //使用 jwt 工具类进行生成
-
- HashMap
map = new HashMap<>(); - map.put("token",jwt);
-
- //4.把完整的用户信息存入 redis ,其中 userId 作为 key
- redisCache.setCacheObject("login:"+userId,loginUser);
-
- return new ResponseResult(200,"登录成功!",map);
- }
-
- }
这里是认证放行接口,同时设置了过滤器的前后顺序
- @Configuration
- public class SecurityConfig extends WebSecurityConfigurerAdapter {
-
-
- /**
- * 【这里是认证放行接口】
- */
- @Bean
- @Override
- protected void configure(HttpSecurity http) throws Exception {
-
- http
-
- //关闭csrf保护机制
- .csrf().disable()
-
- //不通过Session获取SecurityContext,而是通过 Token 进行获取
- .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
- .and()
- .authorizeRequests()
-
- // 对于登录接口 允许匿名访问
- .antMatchers("/user/login").anonymous()
-
- // 除上面外的所有请求全部需要鉴权认证
- .anyRequest().authenticated();
-
- //将 jwt认证过滤器 放在 用户登录过滤器 之前
- http.addFilterBefore(jwtAuthenticationTokenFilter,
- UsernamePasswordAuthenticationFilter.class);
-
- }
-
- }
这里是 JWT认证过滤器,通过请求请求头中发送过来的 Token, 对 Token 中进行解析,从而取出其中的 userId;使用 userId 去 redis 中获取相应的 LoginUser 对象,最后存入 SecurityContextHolder 中(主要用于在整个应用程序中存储和获取用户的安全上下文 )
- @Component
- public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
-
- @Resource
- private RedisCache redisCache;
-
- @Override
- protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException {
-
- //1.从请求头中获取 token
- String token = request.getHeader("token");
- //1.1 若 token 不存在则直接放行(token 不存在说明不需要认证)
- if(!StringUtils.hasLength(token)){
- chain.doFilter(request,response);
- return;
- }
-
- //2.解析 token
- String userId ;
- try {
- Claims claims = JwtUtil.parseJWT(token);
- userId = claims.getSubject();
- } catch (Exception e) {
- e.printStackTrace();
- throw new RuntimeException("token 非法");
- }
-
- //3.从 redis 中获取用户信息
- String redisKey = "login:"+ userId;
-
- LoginUser loginUser = redisCache.getCacheObject(redisKey);
- if(Objects.isNull(loginUser)){
-
- throw new RuntimeException("该用户不存在!");
- }
-
- //4.存入 SecurityContextHolder
- //TODO 获取权限信息
- UsernamePasswordAuthenticationToken authenticationToken
- = new UsernamePasswordAuthenticationToken(loginUser,null,null);
-
- SecurityContextHolder.getContext().setAuthentication(authenticationToken);
-
-
- //5.进行放行
- chain.doFilter(request,response);
- }
前面已经有了用户登录接口,所以现在需要退出登录接口
由于之前在 JwtAuthenticationTokenFilter 过滤器中,已经将登录成功的用户信息放入了 SecurityContextHolder 对象中,所以这里需要将其从中取出;
然后通过用户信息获取到用户 ID,最后在 Redis 中根据对应的用户 ID 删除对应用户的 Redis 缓存信息,这样就可以在下一次登录的时候使之前已经退出的用户 Token 失效,从而需要重新登录,才可访问其他需要授权的页面
- /**
- * 【用户退出登录】
- */
- @Override
- public ResponseResult logout() {
-
- //1.由于用户信息已经保存到了 SecurityContextHolder 中
- // 所以从 SecurityContextHolder 中获取用户 ID
- UsernamePasswordAuthenticationToken authentication
- = (UsernamePasswordAuthenticationToken) SecurityContextHolder.getContext().getAuthentication();
- //1.1 通过 UserDetails 获取用户登录信息
- LoginUser loginUser = (LoginUser) authentication.getPrincipal();
- User user = loginUser.getUser();
- Long userId = user.getId();
-
- //2.根据用户 id 删除 redis 中对应的 key 所对应的 value 值
- redisCache.deleteObject("login:"+userId);
-
- return new ResponseResult(200,"退出登录成功!");
- }
前文引入:
例如一个学校图书馆的管理系统,如果是普通学生登录就能看到借书还书相关的功能,不可能让他看到并且去使用添加书籍信息,删除书籍信息等功能;但是如果是一个图书馆管理员的账号登录了,应该就能看到并使用添加书籍信息,删除书籍信息等功能
所以说,设置权限这一项,对于一个完整的软件而言尤为重要......
第一步:首先在认证放行接口 SecurityConfig 处开启权限控制
@Configuration @EnableGlobalMethodSecurity(prePostEnabled = true) //【开启权限控制】 public class SecurityConfig extends WebSecurityConfigurerAdapter {...............
}
第二步:这里创建一个测试权限的接口,同时设置权限的信息
需要说明的是,@PreAuthorize 注解用于在方法上面设置权限的信息 ,SpEL 表达式定义权限规则,这里的表达式 hasAuthority('test')
表示该方法需要具有"test"权限
- @RestController
- public class HelloController {
-
- @GetMapping("hello")
- @PreAuthorize("hasAuthority('system:dept:list')") //设置权限信息
- public String hello(){
-
- return "hello";
- }
-
- }
第三步:我们需要重写 LoginUser(继承 UserDetals) 中的 getAuthorities() 方法,将继承 UserDetailService 的类 传过来的权限信息封装到 SimpleGrantedAuthority 对象中进行返回
这里是 UserDetailsServiceImpl 类(继承了 UserDetailsService 类),这里的用户权限信息先写死
- @Slf4j
- @Service
- public class UserDetailsServiceImpl implements UserDetailsService {
-
- @Resource
- private UserMappper userMappper;
-
- @Override
- public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
-
- LambdaQueryWrapper
queryWrapper = new LambdaQueryWrapper<>(); -
- queryWrapper.eq(User::getUserName,username);
-
- //1.查询用户信息
- User user = userMappper.selectOne(queryWrapper);
-
- if(Objects.isNull(user)){
- log.error("用户名或密码错误!");
- throw new RuntimeException("用户名或密码错误!");
- }
-
- //2.将用户信息封装为 UserDetails 对象返回
- ArrayList
list = new ArrayList<>(Arrays.asList("test","admin")); //这里权限信息先进行写死 -
- return new LoginUser(user,list); //将用户以及权限信息传入 LoginUser 对象中
- }
-
- }
这里是 LoginUser 类(继承了 UserDetails 类 );其中, SpringSecurity 中的 SimpleGrantedAuthority 对象用于表示授权信息,表示用户被授予的权限或角色
- @Data
- @NoArgsConstructor
- public class LoginUser implements UserDetails {
-
- private User user;
-
- private List
permissions; //创建一个集合,用来封装权限信息 -
- @JSONField(serialize = false) //敏感信息,让 JSON 字符串不包含该字段
- private List
authorities; -
- /**
- * 获取权限信息
- */
- @Override
- public Collection extends GrantedAuthority> getAuthorities() {
-
- //1.若不为空,则直接返回(说明之前已经存在权限信息)
- if(authorities!=null){
- return authorities;
- }
-
- //2.将 permission 中的权限信息封装到 GrantedAuthority 对象中进行返回
- authorities = permissions.stream()
- .map(new Function
() { - @Override
- public SimpleGrantedAuthority apply(String permission) {
-
- SimpleGrantedAuthority simpleGrantedAuthority = new SimpleGrantedAuthority(permission);
- return simpleGrantedAuthority;
- }
- }).collect(Collectors.toList());
-
- 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;
- }
-
-
- public LoginUser(User user, List
permissions) { - this.user = user;
- this.permissions = permissions;
- }
- }
第四步:由于这时候 LoginUser 中的 getAuthorities() 重写方法已存在用户的权限信息,所以经过 JWT 认证过滤器的时候,需要将其存入 SecurityContextHolder 对象中并进行返回,因为在整个 SpringSecurity 框架中,这个对象是连接整个认证流程的上下文
- //4.将用户信息存入 SecurityContextHolder
- //4.1获取权限信息
- Collection extends GrantedAuthority> authorities = loginUser.getAuthorities();
-
- //4.2 将用户信息以及权限信息存入 SecurityContextHolder 对象中
- UsernamePasswordAuthenticationToken authenticationToken
- = new UsernamePasswordAuthenticationToken(loginUser,null,authorities);
-
- SecurityContextHolder.getContext().setAuthentication(authenticationToken);
-
- //5.进行放行
- chain.doFilter(request,response);
第五步:在用户登录退出接口处,使用该上下文对象,获取当前用户权限信息,来进行后续的操作
这里由于用户的权限信息是写死的,平时通常是动态获取的,所以我们选择从数据库中进行动态的获取权限信息
这里使用的是 RBAC 模型,即基于角色的权限控制
如图所示:
这里使用角色的关联表将其他的数据表关联起来
由于存在关联表,所以需要进行多表联查
对应的 SQL 语句如下所示:
- SELECT DISTINCT sm.perms
- FROM sys_user_role sur #用户角色关联表 user_role
-
- LEFT JOIN sys_role sr #角色表 role
- ON sur.role_id = sr.id
-
- LEFT JOIN sys_role_menu srm #角色权限关联表 role_menu
- ON srm.role_id = sur.role_id
-
- LEFT JOIN sys_menu sm #权限表 menu
- ON sm.id = srm.menu_id
-
- WHERE user_id = 2
- AND sr.`status` = 0 AND sm.`status` = 0
MyBatis-Plus 对应的 xml 文件:
- "1.0" encoding="UTF-8" ?>
- mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
-
- <mapper namespace="MyPro.Mapper2.MenuMapper">
-
-
- <select id="selectPermsByUserId" resultType="java.lang.String">
- SELECT DISTINCT (sm.perms)
- FROM sys_user_role sur
-
- LEFT JOIN sys_role sr
- ON sur.role_id = sr.id
-
- LEFT JOIN sys_role_menu srm
- ON srm.role_id = sur.role_id
-
- LEFT JOIN sys_menu sm
- ON sm.id = srm.menu_id
-
- WHERE user_id = {#userId}
- AND sr.`status` = 0 AND sm.`status` = 0
- select>
-
- mapper>
这里进行调用 Mapper 方法,动态的获取用户权限信息:
- @Slf4j
- @Service
- public class UserDetailsServiceImpl implements UserDetailsService {
-
- @Autowired
- private UserMapper userMapper;
-
- @Autowired
- private MenuMapper menuMapper;
-
- @Override
- public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
-
- LambdaQueryWrapper
queryWrapper = new LambdaQueryWrapper<>(); -
- queryWrapper.eq(User::getUserName,username);
-
- //1.查询用户信息
- User user = userMapper.selectOne(queryWrapper);
-
- if(Objects.isNull(user)){
- log.error("用户名或密码错误!");
- throw new RuntimeException("用户名或密码错误!");
- }
-
- //2.将用户信息封装为 UserDetails 对象返回
- // ArrayList
list = new ArrayList<>(Arrays.asList("test","admin")); //这里权限信息先进行写死 -
- List
list = menuMapper.selectPermsByUserId(user.getId()); //这里进行动态的获取用户权限信息 -
- return new LoginUser(user,list); //将用户以及权限信息传入 LoginUser 对象中
- }
-
- }
前言:
在SpringSecurity中,如果我们在认证或者授权的过程中出现了异常会被ExceptionTranslationFilter 捕获到;在 ExceptionTranslationFilter 中会去判断是认证失败还是授权失败出现的异常
如果是认证过程中出现的异常会被封装成 AuthenticationException 然后调用AuthenticationEntryPoint 对象的方法去进行异常处理
如果是授权过程中出现的异常会被封装成 AccessDeniedException 然后调用AccessDeniedHandler 对象的方法去进行异常处理
所以,我们需要进行自定义失败处理,以进行统一的异常处理
这里是【用户认证】的异常处理类:
- @Component
- public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint {
-
- @Override
- public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
-
- ResponseResult result = new ResponseResult(HttpStatus.UNAUTHORIZED.value(),"用户认证失败!");
-
- String json = JSON.toJSONString(result);
-
- //处理异常
- WebUtils.renderString(response,json);
- }
-
- }
这里是【用户授权】的异常处理类:
- @Component
- public class AccessDeniedHandlerImpl implements AccessDeniedHandler {
-
- @Override
- public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException exception) throws IOException, ServletException {
-
- ResponseResult result = new ResponseResult(HttpStatus.UNAUTHORIZED.value(),"您的权限不足!");
-
- String json = JSON.toJSONString(result);
-
- //处理异常
- WebUtils.renderString(response,json);
- }
-
- }
将上面的异常处理器在 Config 类中进行配置,让 SpringSecurity 框架使用自定义处理器:
- protected void configure(HttpSecurity http) throws Exception {
-
- http
- //关闭csrf保护机制
- .csrf().disable()
-
- //不通过Session获取SecurityContext
- .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
- .and()
- .authorizeRequests()
-
- // 对于登录接口 允许匿名访问
- .antMatchers("/user/login").anonymous()
-
- // 除上面外的所有请求全部需要鉴权认证
- .anyRequest().authenticated();
-
- //将 jwt认证过滤器 放在 用户登录过滤器 之前
- http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
-
- //这里是进行配置异常处理器
- http.exceptionHandling()
- .authenticationEntryPoint(authenticationEntryPoint) //用户认证
- .accessDeniedHandler(accessDeniedHandler); //用户授权
- }
前言:
浏览器出于安全的考虑,使用 XMLHttpRequest对象 发起 HTTP请求时必须遵守同源策略(要求源相同才能正常进行通信,即协议、域名、端口号都完全一致),否则就是跨域的HTTP请求,跨域默认情况下是被禁止的
前后端分离项目,前端项目和后端项目一般都不是同源的,所以肯定会存在跨域请求的问题
所以我们就要处理一下,让前端能进行跨域请求
这里对 SpringBoot 配置,进行跨域请求的配置:
- @Configuration
- public class CorsConfig implements WebMvcConfigurer {
-
- @Override
- public void addCorsMappings(CorsRegistry registry) {
-
- // 设置允许跨域的路径
- registry.addMapping("/**")
- // 设置允许跨域请求的域名
- .allowedOriginPatterns("*")
- // 是否允许cookie
- .allowCredentials(true)
- // 设置允许的请求方式
- .allowedMethods("GET", "POST", "DELETE", "PUT")
- // 设置允许的header属性
- .allowedHeaders("*")
- // 跨域允许时间
- .maxAge(3600);
-
- }
-
- }
当然,以上只是对 Spring 进行了配置跨域的请求,还需要对 SpringSecurity 进行跨域的配置
这里,在 Config 配置类中进行跨域的配置:
@Override
protected void configure(HttpSecurity http) throws Exception {
http
//关闭csrf保护机制
.csrf().disable()
//不通过Session获取SecurityContext,而是通过Token进行获取
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
// 对于登录接口 允许匿名访问
.antMatchers("/user/login").anonymous()
// 除上面外的所有请求全部需要鉴权认证
.anyRequest().authenticated();
//将 jwt认证过滤器 放在 用户登录过滤器 之前
http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
//这里是进行配置异常处理器
http.exceptionHandling()
.authenticationEntryPoint(authenticationEntryPoint) //用户认证
.accessDeniedHandler(accessDeniedHandler); //用户授权
//允许跨域
http.cors();
}
}
注意:如果使用 PostMan 进行测试是不会成功的,因为它的本质还是在 “同源策略” 中