松江老城做网站,智慧团建官网登录口入口,3小时网站建设平台,什么网站做问卷好一、Spring Security
Spring Security作为Spring家族的安全框架#xff0c;在安全方面的两个核心功能是认证#xff08;Authentication#xff09;和授权#xff08;Authorization#xff09;。 #xff08;1#xff09;用户认证指的是#xff1a;验证某个用户是否为系…一、Spring Security
Spring Security作为Spring家族的安全框架在安全方面的两个核心功能是认证Authentication和授权Authorization。 1用户认证指的是验证某个用户是否为系统中的合法主体也就是说用户能否访问该系统。用户认证一般要求用户提供用户名和密码系统通过校验用户名和密码来完成认证过程。 通俗点说就是系统认为用户是否能登录 2用户授权指的是验证某个用户是否有权限执行某个操作。在一个系统中不同用户所具有的权限是不同的。比如对一个文件来说有的用户只能进行读取而有的用户可以进行修改。一般来说系统会为不同的用户分配不同的角色而每个角色则对应一系列的权限。 通俗点讲就是系统判断用户是否有权限去做某些事情。 前后端交互
二、Spring Security实现权限
Spring Security的原理是一个过滤器链Security提供了各种功能的过滤器。 要对Web资源进行保护最好的办法莫过于Filter 要想对方法调用进行保护最好的办法莫过于[AOP] 如图所示一个请求想要访问到API就会从左到右经过蓝线框里的过滤器其中绿色部分是负责认证的过滤器蓝色部分是负责异常处理橙色部分则是负责授权。进过一系列拦截最终访问到我们的API。 这里面有两个重要的过滤器UsernamePasswordAuthenticationFilter负责登录认证FilterSecurityInterceptor负责权限授权。 ExceptionTranslationFilter负责过滤器链中抛出的任何AccessDeniedException和AuthenticationException。 说明Spring Security的核心逻辑全在这一套过滤器中过滤器里会调用各种组件完成功能掌握了这些过滤器和组件你就掌握了Spring Security这个框架的使用方式就是对这些过滤器和组件进行扩展。 1、SpringSecurity编码入门 1.1 添加依赖 dependenciesdependencygroupIdorg.springframework.boot/groupIdartifactIdspring-boot-starter-security/artifactId/dependencydependencygroupIdorg.springframework.boot/groupIdartifactIdspring-boot-starter-web/artifactId/dependency/dependencies依赖导入后会引入Security默认的安全功能包括要求经过身份验证的用户才能与应用程序进行交互创建好了默认登录表单生成用户名为user的随机密码并打印在控制台上CSRF攻击防护、Session Fixation攻击防护等。
1.2、启动项目测试 在浏览器访问http://localhost:8800就会弹出security默认的登录用户名是user,结合控制台给出的密码就可以完成登录访问API。正常执行上述操作就说明Spring Security默认安全保护生效。
2、用户认证 概念速查: Authentication接口: 它的实现类表示当前访问系统的用户封装了用户相关信息。 AuthenticationManager接口定义了认证Authentication的方法 UserDetailsService接口加载用户特定数据的核心接口。里面定义了一个根据用户名查询用户信息的方法。 UserDetails接口提供核心用户信息。通过UserDetailsService根据用户名获取处理的用户信息要封装成UserDetails对象返回。然后将这些信息封装到Authentication对象中。
2.1、用户认证核心组件 我们系统中会有许多用户确认当前是哪个用户正在使用我们系统就是登录认证的最终目的。这里我们就提取出了一个核心概念当前登录用户/当前认证用户。整个系统安全都是围绕当前登录用户展开的这个不难理解要是当前登录用户都不能确认了那A下了一个订单下到了B的账户上这不就乱套了。这一概念在Spring Security中的体现就是 Authentication它存储了认证信息代表当前登录用户。
我们在程序中如何获取并使用它呢我们需要通过 SecurityContext 来获取AuthenticationSecurityContext就是我们的上下文对象这个上下文对象则是交由 SecurityContextHolder 进行管理你可以在程序任何地方使用它
Authentication authentication SecurityContextHolder.getContext().getAuthentication();SecurityContextHolder原理就是使用ThreadLocal来保证一个线程中传递同一个对象 现在我们已经知道了Spring Security中三个核心组件
1、Authentication存储了认证信息代表当前登录用户
2、SeucirtyContext上下文对象用来获取Authentication
3、SecurityContextHolder上下文管理对象用来在程序任何地方获取SecurityContext
Authentication中是什么信息呢
1、Principal用户信息没有认证时一般是用户名认证后一般是用户对象
2、Credentials用户凭证一般是密码
3、Authorities用户权限
2.2、用户认证 Spring Security是怎么进行用户认证的呢 AuthenticationManager 就是Spring Security用于执行身份验证的组件只需要调用它的authenticate()方法即可完成认证。Spring Security默认的认证方式就是在UsernamePasswordAuthenticationFilter这个过滤器中进行认证的该过滤器负责认证逻辑。 Spring Security用户认证关键代码如下
// 生成一个包含账号密码的认证信息
Authentication authenticationToken new UsernamePasswordAuthenticationToken(username, passwrod);
// AuthenticationManager校验这个认证信息返回一个已认证的Authentication
Authentication authentication authenticationManager.authenticate(authenticationToken);
// 将返回的Authentication存到上下文中
SecurityContextHolder.getContext().setAuthentication(authentication);
2.2.1、认证接口分析 AuthenticationManager的校验逻辑 根据用户名先查询出用户对象(没有查到则抛出异常)将用户对象的密码和传递过来的密码进行校验密码不匹配则抛出异常。重点是这里每一个步骤Spring Security都提供了组件 1、是谁执行 根据用户名查询出用户对象 逻辑的呢用户对象数据可以存在内存中、文件中、数据库中你得确定好怎么查才行。这一部分就是交由UserDetialsService 处理该接口只有一个方法loadUserByUsername(String username)通过用户名查询用户对象默认实现是在内存中查询。 2、那查询出来的 用户对象 又是什么呢每个系统中的用户对象数据都不尽相同咱们需要确认我们的用户数据是啥样的才行。Spring Security中的用户数据则是由UserDetails 来体现该接口中提供了账号、密码等通用属性。 3、对密码进行校验大家可能会觉得比较简单if、else搞定就没必要用什么组件了吧但框架毕竟是框架考虑的比较周全除了if、else外还解决了密码加密的问题这个组件就是PasswordEncoder负责密码加密与校验。 我们可以看下AuthenticationManager校验逻辑的大概源码
public Authentication authenticate(Authentication authentication) throws AuthenticationException {// 传递过来的用户名String username authentication.getName();// 调用UserDetailService的方法通过用户名查询出用户对象UserDetail查询不出来UserDetailService则会抛出异常UserDetails userDetails this.getUserDetailsService().loadUserByUsername(username);String presentedPassword authentication.getCredentials().toString();// 传递过来的密码String password authentication.getCredentials().toString();// 使用密码解析器PasswordEncoder传递过来的密码是否和真实的用户密码匹配if (!passwordEncoder.matches(password, userDetails.getPassword())) {// 密码错误则抛出异常throw new BadCredentialsException(错误信息...);}// 注意这里返回的已认证Authentication是将整个UserDetails放进去充当PrincipalUsernamePasswordAuthenticationToken result new UsernamePasswordAuthenticationToken(userDetails,authentication.getCredentials(), userDetails.getAuthorities());return result;
}UserDetialsService、UserDetails、PasswordEncoder这三个组件Spring Security都有默认实现这一般是满足不了我们的实际需求的所以这里我们自己来实现这些组件。
2.2.2、加密器PasswordEncoder 采取MD5加密 自定义加密处理组件CustomMd5PasswordEncoder
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Component;
import org.springframework.util.DigestUtils;import java.util.Arrays;/*** 自定义security密码校验*/
public class CustomMd5PasswordEncoder implements PasswordEncoder {Overridepublic String encode(CharSequence rawPassword) {// 进行一个md5加密return Arrays.toString(DigestUtils.md5Digest(rawPassword.toString().getBytes()));}Overridepublic boolean matches(CharSequence rawPassword, String encodedPassword) {// 通过md5校验return encodedPassword.equals(Arrays.toString(DigestUtils.md5Digest(rawPassword.toString().getBytes())));}
}2.2.3、用户对象UserDetails 该接口就是我们所说的用户对象它提供了用户的一些通用属性源码如下
public interface UserDetails extends Serializable {/*** 用户权限集合这个权限对象现在不管它到权限时我会讲解*/Collection? extends GrantedAuthority getAuthorities();/*** 用户密码*/String getPassword();/*** 用户名*/String getUsername();/*** 用户没过期返回true反之则false*/boolean isAccountNonExpired();/*** 用户没锁定返回true反之则false*/boolean isAccountNonLocked();/*** 用户凭据(通常为密码)没过期返回true反之则false*/boolean isCredentialsNonExpired();/*** 用户是启用状态返回true反之则false*/boolean isEnabled();
}实际开发中我们的用户属性各种各样这些默认属性可能是满足不了所以我们一般会自己实现该接口然后设置好我们实际的用户实体对象。实现此接口要重写很多方法比较麻烦我们可以继承Spring Security提供的org.springframework.security.core.userdetails.User类该类实现了UserDetails接口帮我们省去了重写方法的工作
import com.sky.model.system.SysUser;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.User;
import java.util.Collection;
/*** 自定义user对象*/
public class CustomUser extends User {private SysUser sysUser;public CustomUser(SysUser sysUser, Collection? extends GrantedAuthority authorities) {super(sysUser.getUsername(), sysUser.getPassword(), authorities);this.sysUser sysUser;}public SysUser getSysUser() {return sysUser;}public void setSysUser(SysUser sysUser) {this.sysUser sysUser;}
}2.2.4、 业务对象UserDetailsService 该接口很简单只有一个方法
public interface UserDetailsService {/*** 根据用户名获取用户对象获取不到直接抛异常*/UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}实现该接口就完成了自己的业务
import com.sky.model.system.SysUser;
import com.sky.system.custom.CustomUser;
import com.sky.system.service.SysUserService;
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 javax.annotation.Resource;
import java.util.Collections;
import java.util.Objects;/*** 实现UserDetailsService接口重写方法*/
Service
public class UserDetailsServiceImpl implements UserDetailsService {Resourceprivate SysUserService sysUserService;Overridepublic UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {SysUser sysUser sysUserService.queryByUsername(username);if (Objects.isNull(sysUser)){throw new UsernameNotFoundException(用户名不存在);}if(sysUser.getStatus() 0) {throw new RuntimeException(账号已停用);}return new CustomUser(sysUser, Collections.emptyList());}
}2.2.5、登录接口 接下我们需要自定义登陆接口然后让SpringSecurity对这个接口放行,让用户访问这个接口的时候不用登录也能访问。 在接口中我们通过AuthenticationManager的authenticate()方法来进行用户认证,所以需要在SecurityConfig中配置把AuthenticationManager注入容器。 认证成功的话要生成一个jwt放入响应中返回。 Slf4j
Api(tags 系统管理-登录管理)
RequestMapping(/admin/system/index)
RestController
public class IndexController {Resourceprivate SysUserService sysUserService;ApiOperation(登录接口)PostMapping(/login)public ResultMapString,Object login(RequestBody LoginVo loginVo){return sysUserService.login(loginVo);}
}2.2.6、 SecurityConfig配置
package com.sky.system.config;import com.sky.system.custom.CustomMd5PasswordEncoder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import java.util.Collections;
/*** Security配置类*/
Configuration
/*** EnableWebSecurity是开启SpringSecurity的默认行为*/
EnableWebSecurity
public class SecurityConfig {/*** 密码明文加密方式配置* return*/Beanpublic PasswordEncoder passwordEncoder(){return new CustomMd5PasswordEncoder();}/*** 获取AuthenticationManager认证管理器登录时认证使用* param authenticationConfiguration* return* throws Exception*/Beanpublic AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {return authenticationConfiguration.getAuthenticationManager();}Beanpublic SecurityFilterChain filterChain(HttpSecurity http) throws Exception {return http// 基于 token不需要 csrf.csrf().disable()// 开启跨域以便前端调用接口.cors().and().authorizeRequests()// 指定某些接口不需要通过验证即可访问。登录接口肯定是不需要认证的.antMatchers(/admin/system/index/login).permitAll()// 静态资源可匿名访问.antMatchers(HttpMethod.GET, /, /*.html, /**/*.html, /**/*.css, /**/*.js, /profile/**).permitAll().antMatchers(/swagger-ui.html, /swagger-resources/**, /webjars/**, /*/api-docs, /druid/**,/doc.html).permitAll()// 这里意思是其它所有接口需要认证才能访问.anyRequest().authenticated().and()// 基于 token不需要 session.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()// cors security 解决方案.cors().configurationSource(corsConfigurationSource()).and().build();}/*** 配置跨源访问(CORS)* return*/Beanpublic CorsConfigurationSource corsConfigurationSource() {CorsConfiguration configuration new CorsConfiguration();configuration.setAllowedHeaders(Collections.singletonList(*));configuration.setAllowedMethods(Collections.singletonList(*));configuration.setAllowedOrigins(Collections.singletonList(*));configuration.setMaxAge(3600L);UrlBasedCorsConfigurationSource source new UrlBasedCorsConfigurationSource();source.registerCorsConfiguration(/**, configuration);return source;}
}controller通过login方法调用实际业务
Service
public class SysUserServiceImpl extends ServiceImplSysUserMapper, SysUser implements SysUserService {Resourceprivate SysMenuService sysMenuService;/*** 通过AuthenticationManager的authenticate方法来进行用户认证,*/Resourceprivate AuthenticationManager authenticationManager;Overridepublic ResultMapString, Object login(LoginVo loginVo) {// 将表单数据封装到 UsernamePasswordAuthenticationTokenUsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken new UsernamePasswordAuthenticationToken(loginVo.getUsername(), loginVo.getPassword());// authenticate方法会调用loadUserByUsernameAuthentication authenticate authenticationManager.authenticate(usernamePasswordAuthenticationToken);if(Objects.isNull(authenticate)){throw new RuntimeException(用户名或密码错误);}// 校验成功强转对象CustomUser customUser (CustomUser) authenticate.getPrincipal();SysUser sysUser customUser.getSysUser();// 校验通过返回tokenString token JwtUtil.createToken(sysUser.getId(), sysUser.getUsername());MapString, Object map new HashMap();map.put(token,token);return Result.ok(map);}
}2.2.7、认证过滤器 我们需要自定义一个过滤器这个过滤器会去获取请求头中的token对token进行解析取出其中的信息获取对应的LoginUser对象。然后封装Authentication对象存入SecurityContextHolder。
Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {Autowiredprivate RedisCache redisCache;Overrideprotected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {//获取tokenString token request.getHeader(token);if (!StringUtils.hasText(token)) {//放行filterChain.doFilter(request, response);return;}//解析tokenString userid;try {Claims claims JwtUtil.parseJWT(token);userid claims.getSubject();} catch (Exception e) {e.printStackTrace();throw new RuntimeException(token非法);}//从redis中获取用户信息String redisKey login: userid;LoginUser loginUser redisCache.getCacheObject(redisKey);if(Objects.isNull(loginUser)){throw new RuntimeException(用户未登录);}//存入SecurityContextHolder//TODO 获取权限信息封装到Authentication中UsernamePasswordAuthenticationToken authenticationToken new UsernamePasswordAuthenticationToken(loginUser,null,null);SecurityContextHolder.getContext().setAuthentication(authenticationToken);//放行filterChain.doFilter(request, response);}
}3、用户授权 在SpringSecurity中会使用默认的FilterSecurityInterceptor来进行权限校验。在FilterSecurityInterceptor中会从SecurityContextHolder获取其中的Authentication然后获取其中的权限信息。判断当前用户是否拥有访问当前资源所需的权限。 SpringSecurity中的Authentication类
public interface Authentication extends Principal, Serializable {//权限数据列表Collection? extends GrantedAuthority getAuthorities();Object getCredentials();Object getDetails();Object getPrincipal();boolean isAuthenticated();void setAuthenticated(boolean var1) throws IllegalArgumentException;
}前面登录时执行loadUserByUsername()方法时return new CustomUser(sysUser, Collections.emptyList());后面的空数据对接就是返回给Spring Security的权限数据。
在TokenAuthenticationFilter中怎么获取权限数据呢登录时我们把权限数据保存到redis中用户名为key权限数据为value即可这样通过token获取用户名即可拿到权限数据这样就可构成出完整的Authentication对象。
3.1、修改loadUserByUsername()接口方法
Autowired
private SysMenuService sysMenuService;Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {SysUser sysUser sysUserService.getByUsername(username);if(null sysUser) {throw new UsernameNotFoundException(用户名不存在);}if(sysUser.getStatus().intValue() 0) {throw new RuntimeException(账号已停用);}ListString userPermsList sysMenuService.findUserPermsList(sysUser.getId());ListSimpleGrantedAuthority authorities new ArrayList();for (String perm : userPermsList) {authorities.add(new SimpleGrantedAuthority(perm.trim()));}return new CustomUser(sysUser, authorities);
}3.2、修改配置类 修改WebSecurityConfig类 配置类添加注解 开启基于方法的安全认证机制也就是说在web层的controller启用注解机制的安全确认
EnableGlobalMethodSecurity(prePostEnabled true)3.3、控制controller层接口权限 Spring Security默认是禁用注解的要想开启注解需要在继承WebSecurityConfigurerAdapter的类上加EnableGlobalMethodSecurity注解来判断用户对某个控制层的方法是否具有访问权限
通过PreAuthorize标签控制controller层接口权限
public class SysRoleController {Autowiredprivate SysRoleService sysRoleService;PreAuthorize(hasAuthority(bnt.sysRole.list))ApiOperation(value 获取分页列表)GetMapping(/{page}/{limit})public Result index(ApiParam(name page, value 当前页码, required true)PathVariable Long page,ApiParam(name limit, value 每页记录数, required true)PathVariable Long limit,ApiParam(name roleQueryVo, value 查询对象, required false)SysRoleQueryVo roleQueryVo) {PageSysRole pageParam new Page(page, limit);IPageSysRole pageModel sysRoleService.selectPage(pageParam, roleQueryVo);return Result.ok(pageModel);}
}3.4、测试服务器端权限 登录后台分配权限进行测试页面如果添加了按钮权限控制可临时去除方便测试
测试结论 1、分配了权限的能够成功返回接口数据 2、没有分配权限的会抛出异常org.springframework.security.access.AccessDeniedException: 不允许访问
4、异常处理 我们还希望在认证失败或者是授权失败的情况下也能和我们的接口一样返回相同结构的json这样可以让前端能对响应进行统一的处理。要实现这个功能我们需要知道SpringSecurity的异常处理机制。
在SpringSecurity中如果我们在认证或者授权的过程中出现了异常会被ExceptionTranslationFilter捕获到。在ExceptionTranslationFilter中会去判断是认证失败还是授权失败出现的异常。
如果是认证过程中出现的异常会被封装成AuthenticationException然后调用AuthenticationEntryPoint对象的方法去进行异常处理。
如果是授权过程中出现的异常会被封装成AccessDeniedException然后调用AccessDeniedHandler对象的方法去进行异常处理。
所以如果我们需要自定义异常处理我们只需要自定义AuthenticationEntryPoint和AccessDeniedHandler然后配置给SpringSecurity即可。
异常处理有2种方式
1、扩展Spring Security异常处理类AccessDeniedHandler、AuthenticationEntryPoint
2、在spring boot全局异常统一处理
第一种方案说明如果系统实现了全局异常处理那么全局异常首先会获取AccessDeniedException异常要想Spring Security扩展异常生效必须在全局异常再次抛出该异常。
①自定义实现类
import com.alibaba.fastjson2.JSON;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.sky.common.result.ResultCodeEnum;
import com.sky.common.util.WebUtils;
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;
import java.util.HashMap;
import java.util.Map;/*** 认证失败处理*/
Component
public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint {Overridepublic void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {response.setStatus(200);int code ResultCodeEnum.LOGIN_AUTH.getCode();String msg 认证失败无法访问系统资源;response.setContentType(application/json;charsetUTF-8);MapString, Object result new HashMap();result.put(msg, msg);result.put(code, code);String s new ObjectMapper().writeValueAsString(result);response.getWriter().println(s);}
}import com.fasterxml.jackson.databind.ObjectMapper;
import com.sky.common.result.ResultCodeEnum;
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;
import java.util.HashMap;
import java.util.Map;Component
public class AccessDeniedHandlerImpl implements AccessDeniedHandler {Overridepublic void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {int code ResultCodeEnum.PERMISSION.getCode();response.setStatus(200);response.setContentType(application/json;charsetUTF-8);String msg 权限不足无法访问系统资源;MapString, Object result new HashMap();result.put(msg, msg);result.put(code, code);String s new ObjectMapper().writeValueAsString(result);response.getWriter().println(s);}
}②配置给SpringSecurity
先注入对应的处理器 Autowiredprivate AuthenticationEntryPoint authenticationEntryPoint;Autowiredprivate AccessDeniedHandler accessDeniedHandler;然后我们可以使用HttpSecurity对象的方法去配置。 http.exceptionHandling().authenticationEntryPoint(authenticationEntryPoint).accessDeniedHandler(accessDeniedHandler);