若依管理系统RuoYi-Vue:登录和鉴权的实现

news2024/11/23 20:23:12

文章目录

  • 摘要
  • spring-boot-starter-security
  • 验证码生成
    • 是否开启验证码
    • 配置验证码类型
    • CaptchaController的getCode来生成验证码
  • 用户登录
    • SysLoginController的login
    • 验证登录是否正确
    • 用户名密码是否正确
    • Spring Security 的用户名密码验证机制
    • 在SecurityConfig中配置PasswordEncoder
  • 令牌生成
    • 令牌生成介绍
    • 若依令牌生成介绍
  • 鉴权校验
    • 配置 Spring Security
    • 配置访问控制规则
    • 配置 RBAC 规则
    • 配置动态权限规则
    • 实现鉴权逻辑
    • 返回鉴权结果

摘要

若依(RuoYi)是一款基于 Spring Boot 和 Vue.js 的开源权限管理系统,若依登录和鉴权的实现还包含验证码的生成与校验,这是为了增加系统的安全性,防止恶意攻击和暴力破解等行为。其登录和鉴权实现主要包括以下几个步骤:

  1. 验证码生成
  • 前端页面:登录页面中的验证码显示区域,通常位于 Vue.js 的 src/views 目录下。
  • 验证码 API:后端应用的验证码接口。该接口接收请求并生成验证码图片,并将验证码字符保存在 Redis 或者内存中。
  1. 用户登录:用户在登录页面输入用户名和密码,点击登录按钮,将表单数据发送给后端 Spring Boot 应用。
  • 登录 API:后端应用的登录接口,该接口在验证用户名和密码之前,先校验用户输入的验证码是否正确。
  • 验证码校验器:若依中定义了验证码校验器,该校验器会从 Redis 或内存中获取保存的验证码,和用户输入的验证码进行比对,如果匹配成功则返回 true,否则返回 false。
  • 用户验证:验证码通过后会对用户输入的用户名和密码进行验证。若验证通过,则会生成一个令牌(Token)并返回给前端应用。
  1. 令牌生成:令牌通常是一个字符串,可以包含一些用户信息和权限信息,如用户 ID、用户名、角色、权限等。生成令牌的方式有多种,可以使用 JWT(JSON Web Token)、OAuth2 等。Spring Boot 应用将生成的令牌返回给前端应用,前端应用可以将令牌保存在本地存储中,如浏览器的 localStorage。

  2. 鉴权校验:前端应用在每次请求后端接口时,都会将保存在本地的令牌携带在请求头中发送给后端应用。后端应用在接收到请求时,会解析令牌并校验用户的权限信息,确定该用户是否有访问该接口的权限。如果校验通过,则会返回请求的数据;如果校验失败,则会返回权限不足的错误信息。

以上是若依登录和鉴权实现中验证码的生成与校验的主要实现步骤,需要注意的是,在生成验证码时需要使用一些开源的验证码生成工具,如 kaptcha、Google 的 reCAPTCHA 等,而验证码校验的实现需要考虑到安全性和效率等因素,比如使用 Redis 缓存验证码字符、设置验证码有效期等。

spring-boot-starter-security

在ruoyi-common模块中引入了spring-boot-starter-security。

<!-- spring security 安全认证 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>

验证码生成

是否开启验证码

在系统管理,参数设置里,可以设置验证码是否开启,这个参数是存到redis里面的,键值是:sys.account.captchaOnOff。
在这里插入图片描述

配置验证码类型

在ruoyi-admin模块的application.yml里面可以设置验证码类型 math 数组计算 char 字符验证,我比较偏好math,因为输入的时候方便。

  # 验证码类型 math 数组计算 char 字符验证
  captchaType: math

在这里插入图片描述

CaptchaController的getCode来生成验证码

在ruoyi-admin模块的 CaptchaController的getCode方法用于生成验证码。

@GetMapping("/captchaImage")
    public AjaxResult getCode(HttpServletResponse response) throws IOException
    {
        AjaxResult ajax = AjaxResult.success();
        boolean captchaOnOff = configService.selectCaptchaOnOff();
        ajax.put("captchaOnOff", captchaOnOff);
        if (!captchaOnOff)
        {
            return ajax;
        }

        // 保存验证码信息
        String uuid = IdUtils.simpleUUID();
        String verifyKey = Constants.CAPTCHA_CODE_KEY + uuid;

        String capStr = null, code = null;
        BufferedImage image = null;

        // 生成验证码
        if ("math".equals(captchaType))
        {
            String capText = captchaProducerMath.createText();
            capStr = capText.substring(0, capText.lastIndexOf("@"));
            code = capText.substring(capText.lastIndexOf("@") + 1);
            image = captchaProducerMath.createImage(capStr);
        }
        else if ("char".equals(captchaType))
        {
            capStr = code = captchaProducer.createText();
            image = captchaProducer.createImage(capStr);
        }

        redisCache.setCacheObject(verifyKey, code, Constants.CAPTCHA_EXPIRATION, TimeUnit.MINUTES);
        // 转换流信息写出
        FastByteArrayOutputStream os = new FastByteArrayOutputStream();
        try
        {
            ImageIO.write(image, "jpg", os);
        }
        catch (IOException e)
        {
            return AjaxResult.error(e.getMessage());
        }

        ajax.put("uuid", uuid);
        ajax.put("img", Base64.encode(os.toByteArray()));
        return ajax;
    }

用户登录

SysLoginController的login

后端应用的登录接口,在ruoyi-admin模块的 SysLoginController的login方法,该接口接收用户输入的用户名和密码和验证码,并通过调用 Spring Security 的登录方法对用户进行验证,如果验证通过,则生成令牌并返回给前端应用。

	@PostMapping("/login")
    public AjaxResult login(@RequestBody LoginBody loginBody) {
        AjaxResult ajax = AjaxResult.success();
        // 生成令牌
        String token = loginService.login(loginBody.getUsername(), loginBody.getPassword(), loginBody.getCode(),
                loginBody.getUuid());
        ajax.put(Constants.TOKEN, token);
        return ajax;
    }

LoginBody类包含了用户名,密码,验证码以及uuid

@Data
public class LoginBody
{
    /**
     * 用户名
     */
    private String username;

    /**
     * 用户密码
     */
    private String password;

    /**
     * 验证码
     */
    private String code;

    /**
     * 唯一标识
     */
    private String uuid = "";
}

验证登录是否正确

ruoyi-framework模块的SysLoginService的login方法来验证登录是否正确。

/**
     * 登录验证
     *
     * @param username 用户名
     * @param password 密码
     * @param code     验证码
     * @param uuid     唯一标识
     * @return 结果
     */
    public String login(String username, String password, String code, String uuid) {
        boolean captchaOnOff = configService.selectCaptchaOnOff();
        // 验证码开关
        if (captchaOnOff) {
            validateCaptcha(username, code, uuid);
        }
        // 用户验证
        Authentication authentication = null;
        try {
            // 该方法会去调用UserDetailsServiceImpl.loadUserByUsername
            authentication = authenticationManager
                    .authenticate(new UsernamePasswordAuthenticationToken(username, password));
        }
        catch (Exception e)
        {
            if (e instanceof BadCredentialsException)
            {
                AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.password.not.match")));
                throw new UserPasswordNotMatchException();
            }
            else
            {
                AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, e.getMessage()));
                throw new ServiceException(e.getMessage());
            }
        }
        AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_SUCCESS, MessageUtils.message("user.login.success")));
        LoginUser loginUser = (LoginUser) authentication.getPrincipal();
        recordLoginInfo(loginUser.getUser());
        // 生成token
        return tokenService.createToken(loginUser);
    }

ruoyi-framework模块的SysLoginService的validateCaptcha方法用于校验验证码,如果校验失败,会抛出异常。

/**
     * 校验验证码
     * 
     * @param username 用户名
     * @param code 验证码
     * @param uuid 唯一标识
     * @return 结果
     */
    public void validateCaptcha(String username, String code, String uuid)
    {
        String verifyKey = Constants.CAPTCHA_CODE_KEY + uuid;
        String captcha = redisCache.getCacheObject(verifyKey);
        redisCache.deleteObject(verifyKey);
        if (captcha == null)
        {
            AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.expire")));
            throw new CaptchaExpireException();
        }
        if (!code.equalsIgnoreCase(captcha))
        {
            AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.error")));
            throw new CaptchaException();
        }
    }

用户名密码是否正确

authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(username, password));方法会去调用UserDetailsServiceImpl.loadUserByUsername,此时只是根据用户名查看该用户是否存在,账户是否被删除或者停用。

@Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException
    {
        SysUser user = userService.selectUserByUserName(username);
        if (StringUtils.isNull(user))
        {
            log.info("登录用户:{} 不存在.", username);
            throw new ServiceException("登录用户:" + username + " 不存在");
        }
        else if (UserStatus.DELETED.getCode().equals(user.getDelFlag()))
        {
            log.info("登录用户:{} 已被删除.", username);
            throw new ServiceException("对不起,您的账号:" + username + " 已被删除");
        }
        else if (UserStatus.DISABLE.getCode().equals(user.getStatus()))
        {
            log.info("登录用户:{} 已被停用.", username);
            throw new ServiceException("对不起,您的账号:" + username + " 已停用");
        }

        return createLoginUser(user);
    }

Spring Security 的用户名密码验证机制

在 Spring Security 中,用户密码的验证是通过实现 org.springframework.security.core.userdetails.UserDetailsService 接口来完成的。UserDetailsService 接口定义了 loadUserByUsername(String username) 方法,该方法返回一个 UserDetails 对象,该对象包含了用户的基本信息和密码等信息。

具体来说,Spring Security 在用户登录时,会通过调用 loadUserByUsername(String username) 方法,根据用户名从数据库或其他数据源中获取对应的 UserDetails 对象。然后,Spring Security 会使用 PasswordEncoder 对用户输入的密码进行加密,然后将加密后的密码和 UserDetails 对象中存储的密码进行比对。如果两者一致,则认为密码验证通过,否则认为密码验证失败。

PasswordEncoder 是一个密码加密器,它通常由开发者自行配置。Spring Security 提供了多种密码加密器实现,如 BCryptPasswordEncoder、ShaPasswordEncoder 等,开发者可以根据实际需求进行选择。一般情况下,建议使用强安全性的 BCryptPasswordEncoder 加密器,其使用 bcrypt 加密算法,安全性较高。

因此,在 Spring Security 中验证用户密码是否正确的流程如下:

  1. 用户登录时,Spring Security 通过调用 UserDetailsService 的 loadUserByUsername(String username) 方法获取用户的 UserDetails 对象。

  2. Spring Security 将用户输入的密码使用 PasswordEncoder 加密,并将加密后的密码与 UserDetails 对象中存储的密码进行比对。

  3. 如果两者一致,则认为密码验证通过,否则认为密码验证失败,用户登录失败。

需要注意的是,由于密码的验证过程是由 Spring Security 自动完成的,开发者在实现登录接口时,只需要提供用户名和密码即可,无需手动进行密码的验证。

在SecurityConfig中配置PasswordEncoder

/**
     * 强散列哈希加密实现
     */
    @Bean
    public BCryptPasswordEncoder bCryptPasswordEncoder()
    {
        return new BCryptPasswordEncoder();
    }

    /**
     * 身份认证接口
     */
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception
    {
        auth.userDetailsService(userDetailsService).passwordEncoder(bCryptPasswordEncoder());
    }

令牌生成

令牌生成介绍

登录成功后,一般会生成一个令牌(Token),该令牌会被用于后续的请求中,以验证用户的身份。在生成令牌时,一般会使用 JSON Web Token(JWT)技术,JWT 令牌包含了用户的身份信息以及一些元数据。

生成 JWT 令牌的过程一般分为以下几个步骤:

  1. 定义 JWT 的 Header 和 Payload

JWT 令牌由 Header、Payload 和 Signature 三部分组成。Header 包含了加密算法和类型信息,一般采用 Base64 编码。Payload 包含了用户的身份信息和一些元数据,一般采用 JSON 格式表示,例如:

{
  "sub": "1234567890",
  "name": "John Doe",
  "iat": 1516239022
}
  1. 对 JWT 的 Header 和 Payload 进行加密

使用指定的加密算法对 Header 和 Payload 进行加密,一般使用 HMAC 或 RSA 等加密算法。

  1. 生成 Signature

将加密后的 Header 和 Payload 进行拼接,然后使用 Secret Key 对其进行签名,生成 Signature。

  1. 将加密后的 Header、Payload 和 Signature 进行拼接

将加密后的 Header、Payload 和 Signature 进行拼接,一般使用 “.” 进行分隔,得到最终的 JWT 令牌。

生成 JWT 令牌的具体实现方式可以使用现有的 JWT 库,例如 jjwt、Nimbus JOSE + JWT 等。在 Spring Security 中,可以使用 org.springframework.security.jwt.JwtHelper 类来生成 JWT 令牌,该类封装了 JWT 的加密和签名过程。

若依令牌生成介绍

在ruoyi-framework模块下的TokenService的createToken方法可以生成令牌。

/**
     * 从数据声明生成令牌
     *
     * @param claims 数据声明
     * @return 令牌
     */
public String createToken(LoginUser loginUser)
    {
        String token = IdUtils.fastUUID();
        loginUser.setToken(token);
        setUserAgent(loginUser);
        refreshToken(loginUser);

        Map<String, Object> claims = new HashMap<>();
        claims.put(Constants.LOGIN_USER_KEY, token);
        return createToken(claims);
    }
    
    /**
     * 从数据声明生成令牌
     *
     * @param claims 数据声明
     * @return 令牌
     */
private String createToken(Map<String, Object> claims)
    {
        String token = Jwts.builder()
                .setClaims(claims)
                .signWith(SignatureAlgorithm.HS512, secret).compact();
        return token;
    }
/**
     * 刷新令牌有效期
     *
     * @param loginUser 登录信息
     */
    public void refreshToken(LoginUser loginUser)
    {
        loginUser.setLoginTime(System.currentTimeMillis());
        loginUser.setExpireTime(loginUser.getLoginTime() + expireTime * MILLIS_MINUTE);
        // 根据uuid将loginUser缓存
        String userKey = getTokenKey(loginUser.getToken());
        redisCache.setCacheObject(userKey, loginUser, expireTime, TimeUnit.MINUTES);
    }

鉴权校验

前端应用在每次请求后端接口时,都会将保存在本地的令牌携带在请求头中发送给后端应用。后端应用在接收到请求时,会解析令牌并校验用户的权限信息,确定该用户是否有访问该接口的权限。如果校验通过,则会返回请求的数据;如果校验失败,则会返回权限不足的错误信息。

在若依系统中,鉴权校验一般是在接口请求时进行的。Spring Security 提供了丰富的权限控制机制,可以通过配置来实现鉴权校验。

具体实现步骤如下:

配置 Spring Security

在 Spring Security 中配置权限控制,一般包括认证管理器、安全拦截器链、鉴权规则等。可以使用注解或 XML 配置方式实现。

例如,在若依系统中,可以在 SecurityConfig 类中使用 @EnableWebSecurity 注解来开启 Spring Security 配置,并通过 configure(HttpSecurity http) 方法来配置安全拦截器链,例如:

@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;

    /**
     * anyRequest          |   匹配所有请求路径
     * access              |   SpringEl表达式结果为true时可以访问
     * anonymous           |   匿名可以访问
     * denyAll             |   用户不能访问
     * fullyAuthenticated  |   用户完全认证可以访问(非remember-me下自动登录)
     * hasAnyAuthority     |   如果有参数,参数表示权限,则其中任何一个权限可以访问
     * hasAnyRole          |   如果有参数,参数表示角色,则其中任何一个角色可以访问
     * hasAuthority        |   如果有参数,参数表示权限,则其权限可以访问
     * hasIpAddress        |   如果有参数,参数表示IP地址,如果用户IP和参数匹配,则可以访问
     * hasRole             |   如果有参数,参数表示角色,则其角色可以访问
     * permitAll           |   用户可以任意访问
     * rememberMe          |   允许通过remember-me登录的用户访问
     * authenticated       |   用户登录后可访问
     */
    @Override
    protected void configure(HttpSecurity httpSecurity) throws Exception
    {
        httpSecurity
                // CSRF禁用,因为不使用session
                .csrf().disable()
                // 认证失败处理类
                .exceptionHandling().authenticationEntryPoint(unauthorizedHandler).and()
                // 基于token,所以不需要session
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
                // 过滤请求
                .authorizeRequests()
                // 对于登录login 注册register 验证码captchaImage 允许匿名访问
                .antMatchers("/login/*", "/login", "/code/get", "/register", "/captchaImage").anonymous()
                .antMatchers(
                        HttpMethod.GET,
                        "/",
                        "/*.html",
                        "/**/*.html",
                        "/**/*.css",
                        "/**/*.js",
                        "/profile/**"
                ).permitAll()
                .antMatchers("/common/download**").anonymous()
                .antMatchers("/common/download/resource**").anonymous()
                .antMatchers("/swagger-ui.html").anonymous()
                .antMatchers("/swagger-resources/**").anonymous()
                .antMatchers("/webjars/**").anonymous()
                .antMatchers("/*/api-docs").anonymous()
                .antMatchers("/druid/**").anonymous()
                // 除上面外的所有请求全部需要鉴权认证
                .anyRequest().authenticated()
                .and()
                .headers().frameOptions().disable();
        httpSecurity.logout().logoutUrl("/logout").logoutSuccessHandler(logoutSuccessHandler);
        // 添加JWT filter
        httpSecurity.addFilterBefore(authenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
        // 添加CORS filter
        httpSecurity.addFilterBefore(corsFilter, JwtAuthenticationTokenFilter.class);
        httpSecurity.addFilterBefore(corsFilter, LogoutFilter.class);
    }

    // ...
}

在以上代码中,通过 antMatchers() 方法来设置访问控制规则,permitAll() 表示允许所有用户访问,anyRequest().authenticated() 表示需要登录才能访问。其中,jwtAuthenticationTokenFilter 用于对 JWT 令牌进行校验和解析,如果令牌有效,则通过 Spring Security 的认证流程进行身份认证。

下面的代码是httpSecurity.addFilterBefore(corsFilter, JwtAuthenticationTokenFilter.class);中的JwtAuthenticationTokenFilter处理,根据token从redis里获取用户信息:

@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
    private Lock lock = new ReentrantLock();
    @Autowired
    private TokenService tokenService;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
            throws ServletException, IOException {
        LoginUser loginUser = tokenService.getLoginUser(request);
        if (StringUtils.isNotNull(loginUser) && StringUtils.isNull(SecurityUtils.getAuthentication())) {
            tokenService.verifyToken(loginUser);
            UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginUser, null, loginUser.getAuthorities());
            authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
            SecurityContextHolder.getContext().setAuthentication(authenticationToken);
        }
       .......
        chain.doFilter(request, response);
    }
}

根据token从redis里获取用户信息是通过tokenService.getLoginUser(request)来处理的:

public LoginUser getLoginUser(HttpServletRequest request)
    {
        // 获取请求携带的令牌
        String token = getToken(request);
        if (StringUtils.isNotEmpty(token))
        {
            try
            {
                Claims claims = parseToken(token);
                // 解析对应的权限以及用户信息
                String uuid = (String) claims.get(Constants.LOGIN_USER_KEY);
                String userKey = getTokenKey(uuid);
                LoginUser user = redisCache.getCacheObject(userKey);
                return user;
            }
            catch (Exception e)
            {
            }
        }
        return null;
    }

配置访问控制规则

在具体的接口实现中,可以使用 Spring Security 的注解来设置访问控制规则。例如,@PreAuthorize 注解可以在方法或类级别上设置访问控制规则,例如:

@RestController
@RequestMapping("/api/user")
public class UserController {

    @PreAuthorize("hasAuthority('user:view')")
    @GetMapping("/list")
    public ResponseEntity list() {
        // ...
    }

    // ...
}

在以上代码中,@PreAuthorize(“hasAuthority(‘user:view’)”) 表示只有拥有 “user:view” 权限的用户才能访问该接口。如果用户没有该权限,则会返回 403 Forbidden 错误。

配置 RBAC 规则

若依系统还提供了基于角色的访问控制(RBAC),可以使用 @PreAuthorize 注解来设置角色控制规则。例如:

@RestController
@RequestMapping("/api/role")
public class RoleController {

    @PreAuthorize("hasRole('admin')")

在以上代码中,@PreAuthorize(“hasRole(‘admin’)”) 表示只有拥有 “admin” 角色的用户才能访问该接口。如果用户没有该角色,则会返回 403 Forbidden 错误。

配置动态权限规则

若依系统还支持动态权限规则,可以使用 @PreAuthorize 注解结合 SpEL 表达式来实现动态访问控制。例如:

@RestController
@RequestMapping("/api/menu")
public class MenuController {

    @PreAuthorize("@ss.hasPermi('system:menu:add')")
    @PostMapping("/add")
    public ResponseEntity add(@RequestBody Menu menu) {
        // ...
    }

    // ...
}

在以上代码中,@PreAuthorize(“@ss.hasPermi(‘system:menu:add’)”) 表示只有满足 SpEL 表达式 “@ss.hasPermi(‘system:menu:add’)” 的用户才能访问该接口。其中,@ss 是若依系统中自定义的权限控制注解,hasPermi() 是自定义的 SpEL 表达式函数,用于判断用户是否拥有指定的权限。如果用户没有该权限,则会返回 403 Forbidden 错误。
一下代码是RuoYi首创 自定义权限的实现PermissionService,ss取自SpringSecurity首字母。

/**
 * RuoYi首创 自定义权限实现,ss取自SpringSecurity首字母
 * 
 * @author ruoyi
 */
@Service("ss")
public class PermissionService
{
    /** 所有权限标识 */
    private static final String ALL_PERMISSION = "*:*:*";

    /** 管理员角色权限标识 */
    private static final String SUPER_ADMIN = "admin";

    private static final String ROLE_DELIMETER = ",";

    private static final String PERMISSION_DELIMETER = ",";

    /**
     * 验证用户是否具备某权限
     * 
     * @param permission 权限字符串
     * @return 用户是否具备某权限
     */
    public boolean hasPermi(String permission)
    {
        if (StringUtils.isEmpty(permission))
        {
            return false;
        }
        LoginUser loginUser = SecurityUtils.getLoginUser();
        if (StringUtils.isNull(loginUser) || CollectionUtils.isEmpty(loginUser.getPermissions()))
        {
            return false;
        }
        return hasPermissions(loginUser.getPermissions(), permission);
    }

    /**
     * 验证用户是否不具备某权限,与 hasPermi逻辑相反
     *
     * @param permission 权限字符串
     * @return 用户是否不具备某权限
     */
    public boolean lacksPermi(String permission)
    {
        return hasPermi(permission) != true;
    }

    /**
     * 验证用户是否具有以下任意一个权限
     *
     * @param permissions 以 PERMISSION_NAMES_DELIMETER 为分隔符的权限列表
     * @return 用户是否具有以下任意一个权限
     */
    public boolean hasAnyPermi(String permissions)
    {
        if (StringUtils.isEmpty(permissions))
        {
            return false;
        }
        LoginUser loginUser = SecurityUtils.getLoginUser();
        if (StringUtils.isNull(loginUser) || CollectionUtils.isEmpty(loginUser.getPermissions()))
        {
            return false;
        }
        Set<String> authorities = loginUser.getPermissions();
        for (String permission : permissions.split(PERMISSION_DELIMETER))
        {
            if (permission != null && hasPermissions(authorities, permission))
            {
                return true;
            }
        }
        return false;
    }

    /**
     * 判断用户是否拥有某个角色
     * 
     * @param role 角色字符串
     * @return 用户是否具备某角色
     */
    public boolean hasRole(String role)
    {
        if (StringUtils.isEmpty(role))
        {
            return false;
        }
        LoginUser loginUser = SecurityUtils.getLoginUser();
        if (StringUtils.isNull(loginUser) || CollectionUtils.isEmpty(loginUser.getUser().getRoles()))
        {
            return false;
        }
        for (SysRole sysRole : loginUser.getUser().getRoles())
        {
            String roleKey = sysRole.getRoleKey();
            if (SUPER_ADMIN.equals(roleKey) || roleKey.equals(StringUtils.trim(role)))
            {
                return true;
            }
        }
        return false;
    }

    /**
     * 验证用户是否不具备某角色,与 isRole逻辑相反。
     *
     * @param role 角色名称
     * @return 用户是否不具备某角色
     */
    public boolean lacksRole(String role)
    {
        return hasRole(role) != true;
    }

    /**
     * 验证用户是否具有以下任意一个角色
     *
     * @param roles 以 ROLE_NAMES_DELIMETER 为分隔符的角色列表
     * @return 用户是否具有以下任意一个角色
     */
    public boolean hasAnyRoles(String roles)
    {
        if (StringUtils.isEmpty(roles))
        {
            return false;
        }
        LoginUser loginUser = SecurityUtils.getLoginUser();
        if (StringUtils.isNull(loginUser) || CollectionUtils.isEmpty(loginUser.getUser().getRoles()))
        {
            return false;
        }
        for (String role : roles.split(ROLE_DELIMETER))
        {
            if (hasRole(role))
            {
                return true;
            }
        }
        return false;
    }

    /**
     * 判断是否包含权限
     * 
     * @param permissions 权限列表
     * @param permission 权限字符串
     * @return 用户是否具备某权限
     */
    private boolean hasPermissions(Set<String> permissions, String permission)
    {
        return permissions.contains(ALL_PERMISSION) || permissions.contains(StringUtils.trim(permission));
    }
}

实现鉴权逻辑

在具体的接口实现中,需要根据访问控制规则和用户权限信息来实现鉴权逻辑。例如,在若依系统中,可以使用 SecurityUtils 工具类来获取当前登录用户的信息,例如:

@RestController
@RequestMapping("/api/user")
public class UserController {

    @PreAuthorize("hasAuthority('user:view')")
    @GetMapping("/list")
    public ResponseEntity list() {
        List<User> userList = userService.selectUserList();
        return ResponseEntity.ok(userList);
    }

    // ...
}

在以上代码中,通过 SecurityUtils.getSubject().getPrincipal() 方法来获取当前登录用户的信息,如果用户没有登录,则该方法返回 null。根据用户信息和访问控制规则,可以判断用户是否有权限访问该接口。

返回鉴权结果

如果用户没有权限访问接口,则需要返回相应的错误信息。在若依系统中,一般会返回 403 Forbidden 错误或自定义的错误码和错误信息。例如:

@RestControllerAdvice
public class RestExceptionHandler {

    @ExceptionHandler(AccessDeniedException.class)
    public ResponseEntity handleAccessDeniedException(AccessDeniedException e) {
        return ResponseEntity.status(HttpStatus.FORBIDDEN).body(Result.error(ResultCode.FORBIDDEN));
    }

    // ...
}

在以上代码中,通过 @RestControllerAdvice 注解和 @ExceptionHandler 注解来实现全局的异常处理,当出现 AccessDeniedException 异常时,返回 HTTP 403 Forbidden 错误和自定义的错误码和错误信息。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/527160.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

聚观早报|恒大公告许家印成被执行人;特斯拉回应召回超百万辆车

今日要闻&#xff1a;恒大公告&#xff1a;许家印成被执行人&#xff1b;特斯拉回应召回超百万辆车&#xff1b;ChatGPT联网插件下周开放票&#xff1b;天翼物联发布首个3AZ亿级物联网平台&#xff1b;苹果MR头显功能预计远超竞争对手 恒大公告&#xff1a;许家印成被执行人 12…

【沐风老师】3DMAX一键种草插件GrassScatter使用方法详解

GrassScatter for 3dMax一键种草插件使用教程 3DMAX一键种草插件GrassScatter&#xff0c;用于控制草的创建和散布&#xff0c;快速生成草坪&#xff01; 【版本要求】 3dMax2012及更高版本 【安装方法】 方法一&#xff1a;本插件无需安装&#xff0c;使用时直接拖动插件脚…

什么是独享数据库(Database per Microservice)?解决了什么问题?

独享数据库&#xff08;Database per Microservice&#xff09;是一种微服务架构模式&#xff0c;涉及为每个微服务创建单独的数据库。在这种模式下&#xff0c;每个微服务都有自己的数据库&#xff0c;这允许更大的可扩展性、灵活性和自治性。 使用这种模式&#xff0c;每个微…

考考驾照(安卓)

软件安装好后&#xff0c;不用注册登录直接就可以用&#xff0c;软件里主要包含有科目一&#xff0c;科目二&#xff0c;科目三&#xff0c;科目四等等学习的知识内容和一些技巧。 像科目一和科目四有考前须知&#xff0c;考试技巧&#xff0c;和学车流程&#xff0c;还有考试一…

JavaScript学习-DOM事件基础

DOM事件基础 事件监听(绑定)事件监听案例&#xff1a;关闭广告随机点名案例事件监听版本 事件类型类型鼠标经过与鼠标离开轮播图完整版焦点事件键盘事件与文本事件评论字数统计 事件对象获取事件对象事件对象常用属性案例&#xff1a;评论回车发布 环境对象回调函数综合案例&am…

MPLS格式和802.1q帧格式,ISL格式

一.MPLS IETF开发的多协议标记交换&#xff08;MPLS)把第2层的链路状态信息&#xff08;带宽、延迟、利用率等&#xff09;集成到第3层的协议数据单元中&#xff0c;从而简化和改进了第3层分组的交换过程 。理论上&#xff0c;MPLS支持任何第2层和第3层协议。MPLS包头的位置界…

常见电子元器件

目录 常见电子元器件NTC(负温度系数热敏电阻)压敏电阻X2电容(抑制电源电磁干扰用电容器)泄放电阻共模电压共模电感整流桥滤波电容RCD吸收二极管Y电容整流器的原理输出整流肖特基二极管 功率晶体管&#xff08;GTR&#xff0c;三极管)双极型晶体管(BJT&#xff0c;三极管)MOSFET…

Linux安装使用PostgreSQL

安装PostgreSQL 开源数据库&#xff1a;PostgreSQL 在官网选择对应版本的安装包 https://www.postgresql.org/download/ 我的Linux系统是CentOS7 选择对应的系统 选择安装的版本、平台、架构 复制粘贴安装脚本运行 初始化后会创建一个用户postgres&#xff0c;一般开始…

基于javaweb(springboot+mybatis)网上家具商城项目设计和实现以及文档报告

基于javaweb(springbootmybatis)网上家具商城项目设计和实现以及文档报告 博主介绍&#xff1a;5年java开发经验&#xff0c;专注Java开发、定制、远程、指导等,csdn特邀作者、专注于Java技术领域 作者主页 超级帅帅吴 Java项目精品实战案例《500套》 欢迎点赞 收藏 ⭐留言 文末…

内存对齐为何会提高性能?

文章目录 前言一、内存对齐是什么&#xff1f;二、这一行是什么?高速缓存行: CacheLine为啥补齐到64? 总结 前言 著名的Java并发编发锁编程大师Doug lea在JDK 7的并发包里新增一个队列集合LinkedTransferQueue&#xff0c;它在使用volatile变量时&#xff0c;用一种追加字节…

用于鲁棒分割的矢量量化

文章目录 Vector Quantisation for Robust Segmentation摘要本文方法Quantisation for RobustnessPerturbation Bounds 实验结果 Vector Quantisation for Robust Segmentation 摘要 背景 医学领域中分割模型的可靠性取决于模型对输入空间中扰动的鲁棒性。鲁棒性是医学成像中…

读SQL进阶教程笔记16_SQL优化让SQL飞起来

1. 查询速度慢并不只是因为SQL语句本身&#xff0c;还可能是因为内存分配不佳、文件结构不合理等其他原因 1.1. 都是为了减少对硬盘的访问 2. 不同代码能够得出相同结果 2.1. 从理论上来说&#xff0c;得到相同结果的不同代码应该有相同的性能 2.2. 遗憾的是&#xff0c;查…

MySQL 高级语句

实验准备&#xff1a; 第一张表&#xff1a; create table location (Region char(20),Store_Name char(20)); insert into location values(East,Boston); insert into location values(East,New York); insert into location values(West,Los Angeles); insert into location…

基于html+css的图展示70

准备项目 项目开发工具 Visual Studio Code 1.44.2 版本: 1.44.2 提交: ff915844119ce9485abfe8aa9076ec76b5300ddd 日期: 2020-04-16T16:36:23.138Z Electron: 7.1.11 Chrome: 78.0.3904.130 Node.js: 12.8.1 V8: 7.8.279.23-electron.0 OS: Windows_NT x64 10.0.19044 项目…

【3.0版本】用ChatGPT开发一个书籍推荐微信小程序(三)

文章目录 1 前言1.1 实现原理1.2 在小程序如何衔接1.3 技术架构 2 爬取数据2.1 根据书名爬取信息2.2 根据作者爬取信息2.3 根据类型爬取信息 3 讨论 1 前言 1.1 实现原理 万变不离其宗&#xff0c;还是相当于与ChatGPT交互问答&#xff0c;然后映射到小程序中&#xff0c;以下…

Linux -- 进程信号

文章目录 1. 信号量1.1 进程互斥概念1.2 认识信号量 2. 信号入门2.1 信号概念2.2 见一见2.3 signal()系统调用2.4 宏定义信号 3. 信号产生方式3.1 键盘产生信号3.2 系统调用产生信号3.3 软件条件产生信号3.4 硬件异常产生信号3.5 Core dump 4. 阻塞信号4.1 相关概念4.2 信号在内…

Raspberry Pi OS 2023-05-03 版本发布

导读树莓派 Raspberry Pi Foundation 今天发布了用于 Raspberry Pi 计算机的官方 Raspberry Pi OS 发行版新版本&#xff0c;带来了更新的组件、错误 Bug 修复和各种性能改进。 Raspberry Pi OS 2023-05-03 版本最大变化是内核从长期支持的 Linux 5.15 LTS 到长期支持的 Linux …

【为什么可以相信一个HTTPS网站】

解决信用&#xff0c;仅仅有加密和解密是不够的。加密解密解决的只是传输链路的安全问题&#xff0c;相当于两个人说话不被窃听。可以类比成你现在生活 的世界——货币的信用&#xff0c;是由政府在背后支撑的&#xff1b;购房贷款的信用&#xff0c;是由银行在背后支撑的&…

### Cause: dm.jdbc.driver.DMException: 列[URI]长度超出定义

### Cause: dm.jdbc.driver.DMException: 列[URI]长度超出定义 报错信息&#xff1a; ### Cause: dm.jdbc.driver.DMException: 列[URI]长度超出定义 ; 列[URI]长度超出定义; nested exception is dm.jdbc.driver.DMException: 列[URI]长度超出定义at org.springframework.jdb…

计算机毕业论文内容参考|基于Python的互联网金融产品交易平台的设计与实现

文章目录 导文摘要前言绪论课题背景国内外现状与趋势课题内容相关技术与方法介绍技术分析需求分析技术分析技术设计系统架构技术实现产品管理模块订单管理模块支付管理模块总结与展望导文 基于Python的互联网金融产品交易平台的设计与实现 摘要 本文提出并实现了一种基于Pytho…