流程分析
流程说明:
- 客户端发起一个请求,进入 Security 过滤器链。当到 LogoutFilter 的时候判断是否是登出路径,如果是登出路径则到 logoutHandler ,如果登出成功则到 logoutSuccessHandler 登出成功处理。
- 如果不是登出路径则直接进入下一个过滤器。当到 UsernamePasswordAuthenticationFilter 的时候判断是否为登录路径,如果是,则进入该过滤器进行登录操作,如果登录失败则到 AuthenticationFailureHandler ,登录失败处理器处理,如果登录成功则到 AuthenticationSuccessHandler 登录成功处理器处理,如果不是登录请求则不进入该过滤器。
- 进入认证BasicAuthenticationFilter进行用户认证,成功的话会把认证了的结果写入到SecurityContextHolder中SecurityContext的属性authentication上面。如果认证失败就会交给AuthenticationEntryPoint认证失败处理类,或者抛出异常被后续ExceptionTranslationFilter过滤器处理异常,如果是AuthenticationException就交给AuthenticationEntryPoint处理,如果是AccessDeniedException异常则交给AccessDeniedHandler处理。
- 当到 FilterSecurityInterceptor 的时候会拿到 uri ,根据 uri 去找对应的鉴权管理器,鉴权管理器做鉴权工作,鉴权成功则到 Controller 层,否则到 AccessDeniedHandler 鉴权失败处理器处理。
ok,上面我们说的流程中涉及到几个组件,有些是我们需要根据实际情况来重写的。因为我们是使用json数据进行前后端数据交互,并且我们返回结果也是特定封装的。我们先再总结一下我们需要了解的几个组件:
- LogoutFilter - 登出过滤器
- logoutSuccessHandler - 登出成功之后的操作类
- UsernamePasswordAuthenticationFilter - from提交用户名密码登录认证过滤器
- AuthenticationFailureHandler - 登录失败操作类
- AuthenticationSuccessHandler - 登录成功操作类
- BasicAuthenticationFilter - Basic身份认证过滤器
- SecurityContextHolder - 安全上下文静态工具类
- AuthenticationEntryPoint - 认证失败入口
- ExceptionTranslationFilter - 异常处理过滤器
- AccessDeniedHandler - 权限不足操作类
- FilterSecurityInterceptor - 权限判断拦截器、出口
用户认证
首先我们来解决用户认证问题,分为首次登陆,和二次认证。
- 首次登录认证:用户名、密码和验证码完成登录
- 二次token认证:请求头携带Jwt进行身份认证
生成验证码
导入依赖
<!-- springBoot整合redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!--图片验证码-->
<dependency>
<groupId>com.github.axet</groupId>
<artifactId>kaptcha</artifactId>
<version>0.0.9</version>
</dependency>
<!-- hutool工具类-->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.3.3</version>
</dependency>
这里使用到了Redis工具类
Redis工具类参考博客
记得配置Redis序列化规则
数据库访问使用到了MybatisPuls
自行参考其他MP相关文档
配置验证码生成规则
@Configuration
public class KaptchaConfig {
@Bean
public DefaultKaptcha producer() {
Properties properties = new Properties();
//边框
properties.put("kaptcha.border", "no");
//文本颜色
properties.put("kaptcha.textproducer.font.color", "black");
//字符串空行
properties.put("kaptcha.textproducer.char.space", "4");
//高度
properties.put("kaptcha.image.height", "40");
//宽
properties.put("kaptcha.image.width", "120");
//字符大小
properties.put("kaptcha.textproducer.font.size", "30");
Config config = new Config(properties);
DefaultKaptcha defaultKaptcha = new DefaultKaptcha();
defaultKaptcha.setConfig(config);
return defaultKaptcha;
}
}
生成验证码
@Api(tags = "图片验证码相关")
@RestController
public class AuthController extends BaseController{
@Autowired
private Producer producer;
@ApiOperation("生成图片验证码")
@GetMapping("/captcha")
public Result captcha() throws IOException {
String key = "aaaaa";//UUID.randomUUID().toString();
//生成验证码 5位数随机验证码
String code = "11111";//producer.createText();
//生成图片
BufferedImage image=producer.createImage(code);
ByteArrayOutputStream out = new ByteArrayOutputStream();
ImageIO.write(image,"jpg",out);
//转换成base64位编码
BASE64Encoder encoder = new BASE64Encoder();
String str="data:image/jpeg;base64,";//前缀
String base64Img = str + encoder.encode(out.toByteArray());
redisUtil.hset(Const.CAPTCHA_KEY,key,code,120);
return Result.success(
MapUtil.builder().put("token", key)
.put("captchaImg", base64Img)
.build()
);
}
}
生成JWT
导入依赖
<!-- jwt -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
JWT 工具类
/**
* JWT
* @author Tu_Yooo
* @Date 2021/5/25 15:54
*/
@Data
@Component
@ConfigurationProperties(prefix = "tutony.jwt")
public class JwtUtils {
//过期时间
private Long expire;
//密钥
private String secret;
//JWT名称
private String header;
//生成Jwt
public String generateToken(String username){
Date nowDate = new Date();
Date expireDate = new Date(nowDate.getTime() + 1000 * expire );
return Jwts.builder()
.setHeaderParam("typ", "JWT")
.setSubject(username)
.setIssuedAt(nowDate)
.setExpiration(expireDate)// 7天過期
.signWith(SignatureAlgorithm.HS512, secret)
.compact();
}
//解析Jwt
public Claims getClaimsByToken(String jwt){
try{
return Jwts.parser()
.setSigningKey(secret) //密钥
.parseClaimsJws(jwt)
.getBody();
}catch (Exception e){
return null;
}
}
//判断Jwt是否过期
public boolean isTokenExpired(Claims claims){
return claims.getExpiration().before(new Date());
}
}
yml配置JWT
# JWT
tutony:
jwt:
header: Authorization
expire: 604800 # 7天秒单位
secret: f4e2e52034348f86b67cde581c0f9eb5
自定义Security登录成功或失败处理
由于我们是前后端分离项目,无论是否登录成功返回给前端都应该是一个JSON格式的数据
统一的结果集返回
/**
* 统一的结果集返回
* @author Tu_Yooo
* @Date 2021/5/24 16:50
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Result implements Serializable {
//200是正常,非200表示异常
private Integer code;
// 结果消息
private String msg;
//结果数据
private Object data;
/调用正常时返回///
public static Result success(Integer code,String msg,Object data){
return new Result(code,msg,data);
}
public static Result success(Integer code,Object data){
Result result = new Result();
result.setCode(code);
result.setData(data);
return result;
}
public static Result success(Object data){
Result result = new Result();
result.setCode(200);
result.setData(data);
return result;
}
/异常时返回//
public static Result fail(Integer code,String msg,Object data){
return new Result(code,msg,data);
}
public static Result fail(Integer code,String msg){
Result result = new Result();
result.setCode(code);
result.setMsg(msg);
result.setData(null);
return result;
}
public static Result fail(String msg){
Result result = new Result();
result.setCode(400);
result.setMsg(msg);
result.setData(null);
return result;
}
}
登录成功失败处理
/**
* Security 登录处理
* AuthenticationFailureHandler 失败处理
* AuthenticationSuccessHandler 成功处理
* @author Tu_Yooo
* @Date 2021/5/25 14:23
*/
@Component
public class LoginFailureHandler implements AuthenticationFailureHandler, AuthenticationSuccessHandler {
//登录失败时处理
@Override
public void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
httpServletResponse.setContentType("application/json;charset=UTF-8");
ServletOutputStream out = httpServletResponse.getOutputStream();
Result fail = Result.fail(e.getMessage());
out.write(JSONUtil.toJsonStr(fail).getBytes("UTF-8"));
out.flush();
out.close();
}
@Autowired
private JwtUtils jwtUtils;
//登录成功时处理
@Override
public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
httpServletResponse.setContentType("application/json;charset=UTF-8");
ServletOutputStream out = httpServletResponse.getOutputStream();
//生成JWT,并放置到请求头中
String jwt = jwtUtils.generateToken(authentication.getName());
httpServletResponse.setHeader(jwtUtils.getHeader(),jwt);
Result fail = Result.success(jwt);
out.write(JSONUtil.toJsonStr(fail).getBytes("UTF-8"));
out.flush();
out.close();
}
}
验证码认证过滤器
此过滤器放置在Security用户密码认证过滤器之前,先一步校验验证码,如果通过,继续验证用户名密码,如果不通过则抛出异常
/**
* 验证码认证过滤器
* 图片验证码校验过滤器,在登录过滤器前
* OncePerRequestFilter
* @author Tu_Yooo
* @Date 2021/5/25 14:55
*/
@Component
public class CaptchaFilter extends OncePerRequestFilter {
//自定义Security登录成功或失败处理
@Autowired
private LoginFailureHandler loginFailureHandler;
@Override
protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
String url = httpServletRequest.getRequestURI();
//只需要拦截 /login POST请求
if ("/login".equals(url)&& httpServletRequest.getMethod().equals("POST")){
try{
//校验验证码
vaildata(httpServletRequest);
}catch (CaptchaException e){
//如果不正确,就调整到认证失败处理器
loginFailureHandler.onAuthenticationFailure(httpServletRequest,httpServletResponse,e);
}
}
//验证成功 则继续往下面走
filterChain.doFilter(httpServletRequest,httpServletResponse);
}
@Autowired
private RedisUtil redisUtil;
//校验验证码
private void vaildata(HttpServletRequest request){
//用户输入的验证码
String code = request.getParameter("code");
//验证码 在Redis中的Key
String token = request.getParameter("token");
if (StringUtils.isBlank(token) || StringUtils.isBlank(code)){
throw new CaptchaException("验证码错误");
}
if (!redisUtil.hexists(Const.CAPTCHA_KEY, token)){
throw new CaptchaException("验证码失效");
}
if(!code.equals(redisUtil.hget(Const.CAPTCHA_KEY,token))){
throw new CaptchaException("验证码错误");
}
//清除redis 验证码一次性使用
redisUtil.hdel(Const.CAPTCHA_KEY,token);
}
}
自定义登出配置
清空JWT
/**
* 登出操作 清空数据
* @author Tu_Yooo
* @Date 2021/5/27 10:15
*/
@Component
public class JWTLogoutSuccessHandler implements LogoutSuccessHandler {
@Autowired
private JwtUtils jwtUtils;
@Override
public void onLogoutSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
//如果权限信息不为空 需要进行一个手动的登出操作
if (authentication != null){
new SecurityContextLogoutHandler().logout(httpServletRequest,httpServletResponse,authentication);
}
httpServletResponse.setContentType("application/json;charset=UTF-8");
ServletOutputStream out = httpServletResponse.getOutputStream();
//生成JWT,并放置到请求头中
httpServletResponse.setHeader(jwtUtils.getHeader(),"");
Result fail = Result.success("登出成功");
out.write(JSONUtil.toJsonStr(fail).getBytes("UTF-8"));
out.flush();
out.close();
}
}
JWT识别过滤器
用户访问其他接口时,我们会判断是否携带JWT,从JWT中取出用户信息,查询数据库此用户关联的权限信息,一并封装到Security中
/**
* 自定义一个过滤器用来进行识别jwt。
* 实现自动登录
* @author Tu_Yooo
* @Date 2021/5/25 17:14
*/
@Slf4j
public class JWTAuthenticationFilter extends BasicAuthenticationFilter {
public JWTAuthenticationFilter(AuthenticationManager authenticationManager) {
super(authenticationManager);
}
@Autowired
private JwtUtils jwtUtils;
@Autowired
private UserDetailsServiceImpl userDetailsService;
@Autowired
private SysUserService sysUserService;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
String header = request.getHeader(jwtUtils.getHeader());
if(StrUtil.isBlankOrUndefined(header)){
chain.doFilter(request,response);
return;
}
//工具类解析JWT
Claims token = jwtUtils.getClaimsByToken(header);
if (token == null){
throw new JwtException("token异常");
}
if (jwtUtils.isTokenExpired(token)){
log.info("jwt过期");
throw new JwtException("token过期");
}
String username = token.getSubject();
//查询数据库-用户名关联的用户
SysUser user = sysUserService.getByUsername(username);
//获取用户权限等信息
UsernamePasswordAuthenticationToken authentication =
// 参数: 用户名 密码 权限信息
new UsernamePasswordAuthenticationToken(username,null,userDetailsService.getUserAuthority(user.getId()));
//后续security就能获取到当前登录的用户信息了,也就完成了用户认证。
SecurityContextHolder.getContext().setAuthentication(authentication);
chain.doFilter(request,response);
}
}
这里使用UserDetailsServiceImpl
查询了用户id关联的权限信息,我们写在后面
异常处理
JWT识别异常
当JWT过期或者出现异常时会交给此过滤器进行统一处理
/**
* JWT识别异常处理
* @author Tu_Yooo
* @Date 2021/5/26 9:53
*/
@Slf4j
@Component
public class JWTAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
log.error("认证失败!未登录!");
httpServletResponse.setContentType("application/json;charset=UTF-8");
httpServletResponse.setStatus(HttpServletResponse.SC_UNAUTHORIZED);//401
ServletOutputStream out = httpServletResponse.getOutputStream();
Result fail = Result.fail("请先登录!");
out.write(JSONUtil.toJsonStr(fail).getBytes("UTF-8"));
out.flush();
out.close();
}
}
其他异常处理
/**
* 其他异常处理
* @author Tu_Yooo
* @Date 2021/5/26 10:01
*/
@Slf4j
@Component
public class JWTAccessDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AccessDeniedException e) throws IOException, ServletException {
log.info("权限不够!!");
httpServletResponse.setContentType("application/json;charset=UTF-8");
httpServletResponse.setStatus(HttpServletResponse.SC_FORBIDDEN);//403
ServletOutputStream out = httpServletResponse.getOutputStream();
Result fail = Result.fail(e.getMessage());
out.write(JSONUtil.toJsonStr(fail).getBytes("UTF-8"));
out.flush();
out.close();
}
}
用户授权
security从数据库中获取用户信息
需要重写 UserDetailsService
/**
* security从数据库中获取用户信息
*
* 需要重写 UserDetailsService
* @author Tu_Yooo
* @Date 2021/5/26 10:31
*/
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
private SysUserService sysUserService;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//数据库中获取用户信息
SysUser sysUser = sysUserService.getByUsername(username);
if (sysUser == null)
throw new UsernameNotFoundException("用户名或密码不正确");
//参数: 用户id 用户名 用户密码 权限信息
return new AccountUser(sysUser.getId(),sysUser.getUsername(),sysUser.getPassword(),getUserAuthority(sysUser.getId()));
}
/**
* 获取用户权限信息 <角色 菜单>
* @param userId 用户id
* @return 权限信息
*/
public List<GrantedAuthority> getUserAuthority(Long userId){
//角色(ROLE_admin) 菜单操作权限 sys:user:list,sys:user:save
String authority = sysUserService.getUserAuthority(userId);
//将字符串通过工具类进行解析
return AuthorityUtils.commaSeparatedStringToAuthorityList(authority);
}
}
返回结果UserDetails
默认实现类是User
,为了方便后续,新增其他字段我们重写它,使用自己的UserDetails
/**
*
* 封装用户信息
*
* UserDetails 默认实现类 User
* 我们需要去重写它 方便日后字段扩展
* @author Tu_Yooo
* @Date 2021/5/26 10:45
*/
public class AccountUser implements UserDetails {
///扩展字段 用户id
private Long userId;
private static final long serialVersionUID = 530L;
private static final Log logger = LogFactory.getLog(User.class);
private String password;//密码
private final String username; //用户名
private final Collection<? extends GrantedAuthority> authorities; //权限信息
private final boolean accountNonExpired;
private final boolean accountNonLocked;
private final boolean credentialsNonExpired;
private final boolean enabled;
public AccountUser(Long userId,String username, String password, Collection<? extends GrantedAuthority> authorities) {
this(userId,username, password, true, true, true, true, authorities);
}
public AccountUser(Long userId,String username, String password, boolean enabled, boolean accountNonExpired, boolean credentialsNonExpired, boolean accountNonLocked, Collection<? extends GrantedAuthority> authorities) {
if (username != null && !"".equals(username) && password != null) {
this.userId=userId;
this.username = username;
this.password = password;
this.enabled = enabled;
this.accountNonExpired = accountNonExpired;
this.credentialsNonExpired = credentialsNonExpired;
this.accountNonLocked = accountNonLocked;
this.authorities = authorities;
} else {
throw new IllegalArgumentException("Cannot pass null or empty values to constructor");
}
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return this.authorities;
}
@Override
public String getPassword() {
return this.password;
}
@Override
public String getUsername() {
return this.username;
}
@Override
public boolean isAccountNonExpired() {
return this.accountNonExpired;
}
@Override
public boolean isAccountNonLocked() {
return this.accountNonLocked;
}
@Override
public boolean isCredentialsNonExpired() {
return this.credentialsNonExpired;
}
@Override
public boolean isEnabled() {
return this.enabled;
}
}
这里我们使用了sysUserService
查询了数据库权限信息
它必须是一段逗号分割的字符串并交给Security的工具类进行解析
如:ROLE_admin,sys:user:list,sys:user:save
数据库大致如下,建表语句我们提供在后面:
查询用户id关联的权限信息和角色,查询完成之后解析成字符串并返回
/**
* 根据用户id 获取用户关联的权限信息
* @param userId 用户id
* @return 权限信息字符串
*/
@Override
public String getUserAuthority(Long userId) {
String authority = "";
//角色 ROLE_admin
List<SysRole> roles = sysRoleService.list(new QueryWrapper<SysRole>()
.inSql("id", "select role_id from sys_user_role where user_id =" + userId));
System.out.println(roles);
if (roles.size() > 0){
authority=roles.stream().map(r -> "ROLE_"+r.getCode()).collect(Collectors.joining(",")).concat(",");
}
//获取菜单操作权限编码
List<Long> menuIds = sysUserMapper.getNavMenuIds(userId);
if (menuIds.size() > 0){
List<SysMenu> menus = sysMenuService.listByIds(menuIds);
String menuPerms = menus.stream().map(m -> m.getPerms()).collect(Collectors.joining(","));
authority = authority.concat(menuPerms);
}
return authority;
}
Security集成
所有的准备工作完成以后,我们需要编写Security配置类,集成所有的过滤器组件
/**
* Security
* @author Tu_Yooo
* @Date 2021/5/25 12:49
*/
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)//开启注解标注 哪些方法需要鉴权 @PreAuthorize("hasRole('admin')") @PreAuthorize("hasAuthority('sys:user:save')")
public class Securityconfig extends WebSecurityConfigurerAdapter {
@Autowired
private LoginFailureHandler loginFailureHandler;
@Autowired
private CaptchaFilter captchaFilter;
//JWT异常处理
@Autowired
private JWTAuthenticationEntryPoint jwtAuthenticationEntryPoint;
// 其他异常处理
@Autowired
private JWTAccessDeniedHandler jwtAccessDeniedHandler;
//自定义JWT识别
@Bean
JWTAuthenticationFilter jwtAuthenticationFilter() throws Exception {
return new JWTAuthenticationFilter(authenticationManager());
}
//数据库加密方式
@Bean
BCryptPasswordEncoder bCryptPasswordEncoder(){
return new BCryptPasswordEncoder();
}
//数据库获取用户信息
@Autowired
private UserDetailsServiceImpl userDetailsService;
//登出
@Autowired
private JWTLogoutSuccessHandler jwtLogoutSuccessHandler;
//不需要拦截的白名单
private static final String[] URL_WHITELIST={
"/login",
"/logout",
"/captcha",
"/favicon.ico",
"/swagger-ui.html"
};
//授权
@Override
protected void configure(HttpSecurity http) throws Exception {
//允许跨域 关闭csrf
http.cors().and().csrf().disable()
//登录配置
.formLogin()
.successHandler(loginFailureHandler) //自定义登录成功时处理
.failureHandler(loginFailureHandler) //自定义登录失败时处理
// 登出配置
.and()
.logout()
.logoutSuccessHandler(jwtLogoutSuccessHandler) //登出成功时处理
//禁用Session
.and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS) //不生成session
//配置拦截规则
.and()
.authorizeRequests()
.antMatchers(URL_WHITELIST).permitAll() //白名单中路径 不需要拦截
.anyRequest().authenticated()
//异常处理器
.and()
.exceptionHandling()
.authenticationEntryPoint(jwtAuthenticationEntryPoint) //JWT异常处理
.accessDeniedHandler(jwtAccessDeniedHandler)//其他异常处理
//配置自定义过滤器
.and()
//自定义JWT识别过滤器
.addFilter(jwtAuthenticationFilter())
//自定义验证码过滤器 在 账户密码过滤器之前执行
.addFilterBefore(captchaFilter, UsernamePasswordAuthenticationFilter.class);
}
//认证
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//数据库认证方式
auth.userDetailsService(userDetailsService);
}
}
解决跨域问题
/**
* MVC扩展配置
* @author Tu_Yooo
* @Date 2021/5/25 12:35
*/
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
/**
* 解决跨域问题
*/
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("*")
.allowedMethods("GET", "HEAD", "POST", "PUT", "DELETE", "OPTIONS")
.allowCredentials(true)
.maxAge(3600)
.allowedHeaders("*");
}
private CorsConfiguration buildConfig() {
CorsConfiguration corsConfiguration = new CorsConfiguration();
corsConfiguration.addAllowedOrigin("*");
corsConfiguration.addAllowedHeader("*");
corsConfiguration.addAllowedMethod("*");
corsConfiguration.addExposedHeader("Authorization");
return corsConfiguration;
}
@Bean
public CorsFilter corsFilter() {
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", buildConfig());
return new CorsFilter(source);
}
}
权限注解说明
当我们集成了Security后,就可以使用它的内置注解来标注controller接口,完成权限控制
Security内置的权限注解:
- @PreAuthorize:方法执行前进行权限检查
- @PostAuthorize:方法执行后进行权限检查
- @Secured:类似于 @PreAuthorize可以在Controller的方法前添加这些注解表示接口需要什么权限。
比如需要Admin角色权限:
@PreAuthorize("hasRole('admin')")
比如需要添加管理员的操作权限
@PreAuthorize("hasAuthority('sys:user:save')")
ok,我们再来整体梳理一下授权、验证权限的流程:
- 用户登录或者调用接口时候识别到用户,并获取到用户的权限信息注解标识
- Controller中的方法需要的权限或角色
- Security通过FilterSecurityInterceptor匹配URI和权限是否匹配有权限则可以访问接口,当无权限的时候返回异常交给AccessDeniedHandler操作类处理
@Autowired
private SysUserService sysUserService;
@PreAuthorize("hasRole('admin')")
@GetMapping("/test")
public Result test(){
return Result.success(sysUserService.list());
}
致谢
本文参考B站UP主MarkerHub视频笔记整理视频链接
参考文档:MP使用及建表语句都在其中哟~