【业务功能篇112】Springboot + Spring Security 权限管理-登录模块开发实战

news2024/11/26 21:52:51
合家云社区物业管理平台

4.权限管理模块研发

4.3 登录模块开发

前台和后台的认证授权统一都使用SpringSecurity安全框架来实现。首次登录过程如下图:

image.png

4.3.1 生成图片校验码

4.3.1.1 导入工具类
(1) 导入Constants 常量类
/**
 * 通用常量类
 * @author spikeCong
 * @date 2023/5/3
 **/
public class Constants {

    /**
     * UTF-8 字符集
     */
    public static final String UTF8 = "UTF-8";

    /**
     * GBK 字符集
     */
    public static final String GBK = "GBK";

    /**
     * http请求
     */
    public static final String HTTP = "http://";

    /**
     * https请求
     */
    public static final String HTTPS = "https://";

    /**
     * 通用成功标识
     */
    public static final String SUCCESS = "0";

    /**
     * 通用失败标识
     */
    public static final String FAIL = "1";

    /**
     * 登录成功
     */
    public static final String LOGIN_SUCCESS = "Success";

    /**
     * 注销
     */
    public static final String LOGOUT = "Logout";

    /**
     * 登录失败
     */
    public static final String LOGIN_FAIL = "Error";

    /**
     * 验证码 redis key
     */
    public static final String CAPTCHA_CODE_KEY = "captcha_codes:";

    /**
     * 登录用户 redis key
     */
    public static final String LOGIN_TOKEN_KEY = "login_tokens:";

    /**
     * 防重提交 redis key
     */
    public static final String REPEAT_SUBMIT_KEY = "repeat_submit:";

    /**
     * 验证码有效期(分钟)
     */
    public static final Integer CAPTCHA_EXPIRATION = 2;

    /**
     * 令牌
     */
    public static final String TOKEN = "token";

    /**
     * 令牌前缀
     */
    public static final String TOKEN_PREFIX = "Bearer ";

    /**
     * 令牌前缀
     */
    public static final String LOGIN_USER_KEY = "login_user_key";

    /**
     * 用户ID
     */
    public static final String JWT_USERID = "userid";

    /**
     * 用户名称
     */
    public static final String JWT_USERNAME = "sub";

    /**
     * 用户头像
     */
    public static final String JWT_AVATAR = "avatar";

    /**
     * 创建时间
     */
    public static final String JWT_CREATED = "created";

    /**
     * 用户权限
     */
    public static final String JWT_AUTHORITIES = "authorities";

    /**
     * 参数管理 cache key
     */
    public static final String SYS_CONFIG_KEY = "sys_config:";

    /**
     * 字典管理 cache key
     */
    public static final String SYS_DICT_KEY = "sys_dict:";

    /**
     * 资源映射路径 前缀
     */
    public static final String RESOURCE_PREFIX = "/profile";

    /**
     * 默认为空消息
     */
    public static final String DEFAULT_NULL_MESSAGE = "暂无承载数据";
    /**
     * 默认成功消息
     */
    public static final String DEFAULT_SUCCESS_MESSAGE = "操作成功";
    /**
     * 默认失败消息
     */
    public static final String DEFAULT_FAILURE_MESSAGE = "操作失败";
}
(2) 导入UUIDUtils

UUID是指在一台机器上生成的数字,它保证对在同一时空中的所有机器都是唯一的。通常平台会提供生成的API。按照开放软件基金会(OSF)制定的标准计算,用到了以太网卡地址、纳秒级时间、芯片ID码和许多可能的数字。

/**
 * UUID生成器工具类
 * @author spikeCong
 * @date 2023/5/3
 **/
public class UUIDUtils {
    /**
     * 获取随机UUID
     *
     * @return 随机UUID
     */
    public static String randomUUID()
    {
        return UUID.randomUUID().toString();
    }

    /**
     * 简化的UUID,去掉了横线
     *
     * @return 简化的UUID,去掉了横线
     */
    public static String simpleUUID()
    {
        return UUID.randomUUID().toString().replaceAll("-", "");
    }
}
(3) 导入Kv类 链式map

链式映射是指在 Java 中使用 Map 接口的一种实现方式,它允许在一个键值映射中进行多次操作而无需创建新的 Map 对象。例如,可以在同一个 Map 中链式地添加、删除或更新键值对。

核心就是 重写 Map 接口的 put() 方法,以返回 this 引用,以实现链式调用

/**
 * 链式Map
 *  继承 LinkedCaseInsensitiveMap, 对key大小写不敏感的LinkedHashMap实现
 * @author spikeCong
 * @date 2023/5/3
 **/
public class ChainedMap extends LinkedCaseInsensitiveMap<Object> {
    private ChainedMap() {
        super();
    }

    /**
     * 创建ChainedMap
     *
     * @return ChainedMap
     */
    public static ChainedMap create() {
        return new ChainedMap();
    }

    public static <K, V> HashMap<K, V> newMap() {
        return new HashMap<>(16);
    }

    /**
     * 设置列
     *
     * @param attr  属性
     * @param value 值
     * @return 本身
     */
    public ChainedMap set(String attr, Object value) {
        this.put(attr, value);
        return this;
    }

    /**
     * 设置全部
     *
     * @param map 属性
     * @return 本身
     */
    public ChainedMap setAll(Map<? extends String, ?> map) {
        if (map != null) {
            this.putAll(map);
        }
        return this;
    }

    /**
     * 设置列,当键或值为null时忽略
     *
     * @param attr  属性
     * @param value 值
     * @return 本身
     */
    public ChainedMap setIgnoreNull(String attr, Object value) {
        if (attr != null && value != null) {
            set(attr, value);
        }
        return this;
    }

    public Object getObj(String key) {
        return super.get(key);
    }

    /**
     * 获得特定类型值
     *
     * @param <T>          值类型
     * @param attr         字段名
     * @param defaultValue 默认值
     * @return 字段值
     */
    @SuppressWarnings("unchecked")
    public <T> T get(String attr, T defaultValue) {
        final Object result = get(attr);
        return (T) (result != null ? result : defaultValue);
    }

    /**
     * 获得特定类型值
     *
     * @param attr 字段名
     * @return 字段值
     */
    public String getStr(String attr) {
        if (null == attr || attr.equals(StringPool.NULL)) {
            return StringPool.NULL;
        }
        return attr;
    }

    /**
     * 获得特定类型值
     *
     * @param attr 字段名
     * @return 字段值
     */
    public Integer getInt(String attr) {
        if (attr == null) {
            return -1;
        }
        try {
            return Integer.valueOf(attr);
        } catch (final NumberFormatException nfe) {
            return -1;
        }
    }

    /**
     * 获得特定类型值
     *
     * @param attr 字段名
     * @return 字段值
     */
    public Long getLong(String attr) {
        if (attr == null) {
            return -1L;
        }
        try {
            return Long.valueOf(attr);
        } catch (final NumberFormatException nfe) {
            return -1L;
        }
    }

    /**
     * 获得特定类型值
     *
     * @param attr 字段名
     * @return 字段值
     */
    public Float getFloat(String attr) {
        if (attr != null) {
            return Float.valueOf(attr.trim());
        }
        return null;
    }

    public Double getDouble(String attr) {
        if (attr != null) {
            return Double.valueOf(attr.trim());
        }
        return null;
    }


    /**
     * 获得特定类型值
     *
     * @param attr 字段名
     * @return 字段值
     */
    public Boolean getBool(String attr) {
        if (attr != null) {
            String val = String.valueOf(attr);
            val = val.toLowerCase().trim();
            return Boolean.parseBoolean(val);
        }
        return null;
    }

    /**
     * 获得特定类型值
     *
     * @param attr 字段名
     * @return 字段值
     */
    public byte[] getBytes(String attr) {
        return get(attr, null);
    }

    /**
     * 获得特定类型值
     *
     * @param attr 字段名
     * @return 字段值
     */
    public Date getDate(String attr) {
        return get(attr, null);
    }

    /**
     * 获得特定类型值
     *
     * @param attr 字段名
     * @return 字段值
     */
    public Time getTime(String attr) {
        return get(attr, null);
    }

    /**
     * 获得特定类型值
     *
     * @param attr 字段名
     * @return 字段值
     */
    public Timestamp getTimestamp(String attr) {
        return get(attr, null);
    }

    /**
     * 获得特定类型值
     *
     * @param attr 字段名
     * @return 字段值
     */
    public Number getNumber(String attr) {
        return get(attr, null);
    }

    @Override
    public ChainedMap clone() {
        ChainedMap clone = new ChainedMap();
        clone.putAll(this);
        return clone;
    }
}
(4) 导入序列化工具类

添加序列化工具类,让Redis使用FastJson序列化,提高序列化效率, 将存储在Redis中的value值,序列化为JSON格式便于查看

public class FastJsonJsonRedisSerializer<T> implements RedisSerializer<T>
(5) 导入Redis工具类
  • 当Redis当做数据库或者消息队列来操作时,我们一般使用RedisTemplate来操作
  • 当Redis作为缓存使用时,我们可以将它作为Spring Cache的实现,直接通过注解使用
@Component
public class RedisCache{}
(6) 导入redis配置类
@Configuration
public class RedisConfig {}
4.3.1.2 生成验证码
(1) 导入依赖
<dependency>
    <groupId>com.github.whvcse</groupId>
    <artifactId>easy-captcha</artifactId>
    <version>1.6.2</version>
</dependency>
(2) application.yml 增加redis配置
# Spring配置
spring:
  # redis 配置
  redis:
    # 地址
    host: localhost
    # 端口,默认为6379
    port: 6379
    # 密码
    password:
    # 连接超时时间
    timeout: 10s
    jedis:
      pool:
        # 连接池中的最小空闲连接
        min-idle: 3
        # 连接池中的最大空闲连接
        max-idle: 8
        # 连接池的最大数据库连接数
        max-active: 8
        # #连接池最大阻塞等待时间(使用负值表示没有限制)
        max-wait: -1ms
(3) 创建CaptchaController
@RestController
public class CaptchaController {

    //当Redis当做数据库或者消息队列来操作时,我们一般使用RedisTemplate来操作
    @Autowired
    private RedisTemplate redisTemplate;

    /**
     * 生成验证码
     * @param response
     * @return: com.mashibing.springsecurity_example.common.ResponseResult
     */
    @GetMapping("/captchaImage")
    public ChainedMap getCode(HttpServletResponse response){
        SpecCaptcha specCaptcha = new SpecCaptcha(130, 48, 4);

        //生成验证码,及验证码唯一标识
        String uuid = UUIDUtils.simpleUUID();
        String key = Constants.CAPTCHA_CODE_KEY + uuid;
        String code = specCaptcha.text().toLowerCase();

        //保存到redis
        redisTemplate.opsForValue().set(key, code, Duration.ofMinutes(30));

        return ChainedMap.create().set("uuid",uuid).set("img",specCaptcha.toBase64());
    }
}
(4) 查看接口文档进行测试

4.3.2 登录接口实现

4.3.2.1 数据库查询用户信息
(1) 创建SysUser类
  • 创建sys_user表对应实体类, com.msb.hjycommunity.system.domain.SysUser
public class SysUser extends BaseEntity {

    /** 用户ID */
    @Excel(name = "用户序号")
    @TableId
    private Long userId;

    /** 部门ID */
    @Excel(name = "部门编号")
    private Long deptId;

    /** 用户账号 */
    @Excel(name = "登录名称")
    private String userName;

    /** 用户昵称 */
    @Excel(name = "用户名称")
    private String nickName;

    /** 用户邮箱 */
    @Excel(name = "用户邮箱")
    private String email;

    /** 手机号码 */
    @Excel(name = "手机号码")
    private String phonenumber;

    /** 用户性别 */
    @Excel(name="用户性别",replace = {"男_0","女_1","未知_0"})
    private String sex;

    /** 用户头像 */
    private String avatar;

    /** 密码 */
    private String password;

    /** 盐加密 */
    private String salt;

    /** 帐号状态(0正常 1停用) */
    @Excel(name = "帐号状态",replace = {"正常_0","停用_1"})
    private String status;

    /** 删除标志(0代表存在 2代表删除) */
    private String delFlag;

    /** 最后登录IP */
    @Excel(name = "最后登录IP")
    private String loginIp;

    /** 最后登录时间 */
    @Excel(name = "最后登录时间", width = 30, format = "yyyy-MM-dd HH:mm:ss")
    private Date loginDate;

    public SysUser() {
    }

    //对 用户名 邮箱 手机号进行校验
    @NotBlank(message = "用户账号不能为空")
    @Size(min = 0, max = 30, message = "用户账号长度不能超过30个字符")    public String getUserName() {
        return userName;
    }

    @Email(message = "邮箱格式不正确")
    @Size(min = 0, max = 50, message = "邮箱长度不能超过50个字符")
    public String getEmail() {
        return email;
    }

    @Size(min = 0, max = 11, message = "手机号码长度不能超过11个字符")
    public String getPhonenumber() {
        return phonenumber;
    }

    //序列化时忽略密码
    @JsonIgnore
    public String getPassword() {
        return password;
    }

    //......
}
(2) 创建 SysUserMapper
public interface SysUserMapper extends BaseMapper<SysUser> {

    /**
     * 通过用户名查询用户
     * @param userName 用户名
     * @return 用户对象信息
     */
    public SysUser selectUserByUserName(String userName);
}
<mapper namespace="com.msb.hjycommunity.system.mapper.SysUserMapper">

    <select id="selectUserByUserName" parameterType="string" resultType="SysUser">

        SELECT * FROM sys_user where user_name = #{userName}
    </select>

</mapper>
  • 测试
@RunWith(SpringRunner.class)
@SpringBootTest
public class TestHjyCommunityApplication {

    @Autowired
    SysUserMapper userMapper;

    @Test
    public void testSelectUserByUserName(){
        SysUser admin = userMapper.selectUserByUserName("admin");
        System.out.println(admin);
    }
}
(3) 创建 SysUserService
public interface SysUserService {
  
    /**
     * 通过用户名查询用户
     * @param userName 
     * @return: com.msb.hjycommunity.system.domain.SysUser
     */
    public SysUser selectUserByUserName(String userName);
}
@Service
@Slf4j
public class SysUserServiceImpl implements SysUserService {

    @Resource
    private SysUserMapper sysUserMapper;

    @Override
    public SysUser selectUserByUserName(String userName) {
        return sysUserMapper.selectUserByUserName(userName);
    }
}
4.3.2.2 引入SpringSecurity
  • 认证流程图

image.png

(1) 实现UserDetailsService接口
/**
 * 用户验证处理
 * @author spikeCong
 * @date 2023/5/3
 **/
@Service
@Slf4j
public class UserDetailsServiceImpl implements UserDetailsService {

    @Autowired
    private SysUserService userService;


    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {

        SysUser user = userService.selectUserByUserName(username);

        if(Objects.isNull(user)){
            log.info("登录用户:{} 不存在",username);
            throw new UsernameNotFoundException("登录用户: " + username + " 不存在");
        }
        else if(UserStatus.DELETED.getCode().equals(user.getDelFlag())){
            log.info("登录用户:{} 已被删除",username);
            throw new BaseException("对不起,您的账号: " + username + " 以被删除" );
        }
        else if(UserStatus.DISABLE.getCode().equals(user.getStatus())){
            log.info("登录用户:{} 已被停用",username);
            throw new BaseException("对不起,您的账号: " + username + " 以被停用" );
        }
  
        return createLoginUser(user);
    }

    public UserDetails createLoginUser(SysUser user) {
  
        return new LoginUser(user);
    }
}
  • 用户状态枚举
/**
 * 用户状态
 * @author spikeCong
 * @date 2023/5/3
 **/
public enum UserStatus {

    OK("0","正常"),DISABLE("1","停用"),DELETED("2","删除");
  
    private final String code;
    private final String info;

    UserStatus(String code, String info) {
        this.code = code;
        this.info = info;
    }

    public String getCode() {
        return code;
    }
    public String getInfo() {
        return info;
    }
}
  • LoginUser
/**
 * 登录用户 身份权限对象
 * @author spikeCong
 * @date 2023/5/3
 **/
public class LoginUser implements UserDetails {

    private SysUser user;

    public LoginUser(SysUser user) {
        this.user = user;
    }
    /**
     *  用于获取用户被授予的权限,可以用于实现访问控制。
     */
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return null;
    }

    /**
     * 用于获取用户的密码,一般用于进行密码验证。
     */
    @Override
    public String getPassword() {
        return user.getPassword();
    }

    /**
     * 用于获取用户的用户名,一般用于进行身份验证。
     */
    @JsonIgnore
    @Override
    public String getUsername() {
        return user.getPassword();
    }

    /**
     * 用于判断用户的账户是否未过期,可以用于实现账户有效期控制。
     */
    @JsonIgnore
    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    /**
     * 用于判断用户的账户是否未锁定,可以用于实现账户锁定功能。
     */
    @JsonIgnore
    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    /**
     * 用于判断用户的凭证(如密码)是否未过期,可以用于实现密码有效期控制。
     */
    @JsonIgnore
    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    /**
     * 用于判断用户是否已激活,可以用于实现账户激活功能。
     */
    @JsonIgnore
    @Override
    public boolean isEnabled() {
        return true;
    }
}
(2) 编写配置类 SecurityConfig
  • 包路径 com.msb.hjycommunity.framework.security.SecurityConfig
/**
 * Security配置
 * @author spikeCong
 * @date 2023/5/3
 **/
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    /**
     * 认证失败处理器
     */
    @Autowired
    private AuthenticationEntryPoint unauthorizedHandler;


    /**
     * 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 http) throws Exception {

        http
                // CSRF禁用,因为不使用session
                .csrf().disable().sessionManagement()
                //基于token,所以不需要session
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS);

        http
                //过滤请求
                .authorizeRequests()
                // 对于登录login 验证码captchaImage 允许匿名访问
                .mvcMatchers("/login","/captchaImage").anonymous()
                // 除上面外的所有请求全部需要鉴权认证
                .anyRequest().authenticated();
        http
                //认证失败处理器
                .exceptionHandling().authenticationEntryPoint(unauthorizedHandler);

        //添加JWTFilter

        //添加 CORS filter
    }

    /*
     * 配置密码加密方式
     */
    @Bean
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }

}

securedEnabled: 开启 Spring Security 提供的 @Secured 注解支持,该注解不支持权限表达式

(3) 添加自定义认证失败处理器
@Component
public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint, Serializable {

    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
        //状态码 401
        Integer code = HttpStatus.UNAUTHORIZED;
        ServletUtils.renderString(response, JSON.toJSONString(BaseResponse.fail(code.toString(),"认证失败,无法访问系统资源")));
    }
}
4.3.2.3 自定义登录接口
(1) 创建用户登录对象
//com.msb.hjycommunity.system.domain.vo.LoginBody
  
/**
 * 用户登录对象
 * @author spikeCong
 * @date 2023/5/4
 **/
public class LoginBody {

    /**
     * 用户名
     */
    private String username;

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

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

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

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    public String getCode() {
        return code;
    }

    public void setCode(String code) {
        this.code = code;
    }

    public String getUuid() {
        return uuid;
    }

    public void setUuid(String uuid) {
        this.uuid = uuid;
    }
}

(2) 创建LoginService
 public interface SysLoginService {
 
     public String login(String username, String password, String code, String uuid);
 }
 /**
  * 登录校验
  * @author spikeCong
  * @date 2023/5/4
  **/
 @Component
 public class SysLoginServiceImpl implements SysLoginService {
 
     @Autowired
     private AuthenticationManager authenticationManager;
 
     @Autowired
     private RedisCache redisCache;
 
     /**
      * 带验证码登录
      * @param username
      * @param password
      * @param code
      * @param uuid
      * @return: java.lang.String
      */
     @Override
     public String login(String username, String password, String code, String uuid) {
 
         //1.从redis中获取验证码,判断是否正确
         String verifyKey = Constants.CAPTCHA_CODE_KEY + uuid;
         String captcha = redisCache.getCacheObject(verifyKey);
         redisCache.deleteObject(verifyKey);
 
         if (captcha == null || !code.equalsIgnoreCase(captcha)){
             throw new CaptchaNotMatchException("验证码错误!");
         }
 
         //2.进行用户认证
         Authentication authentication = null;
         try {
             //该方法会去调用UserDetailsServiceImpl.loadUserByUsername
             authentication = authenticationManager
                     .authenticate(new UsernamePasswordAuthenticationToken(username, password));
         }catch (Exception e){
             throw new BaseException("用户不存在或密码错误!");
         }
         
         //3. 获取经过身份验证的用户的主体信息
         LoginUser loginUser = (LoginUser) authentication.getPrincipal();
         
         //4.调用TokenService 生成token
         return tokenService.createToken(loginUser);
     }
 }

**验证码验证错误异常 **

 //com.msb.hjycommunity.common.core.exception.CaptchaNotMatchException
 /**
  * 验证码异常
  * @author spikeCong
  * @date 2023/5/4
  **/
 public class CaptchaNotMatchException extends BaseException {
 
     public CaptchaNotMatchException(String defaultMessage) {
         super(defaultMessage);
     }
 }
(3) 创建TokenService

主配置文件中添加token相关配置

 # token配置
 token:
   # 令牌自定义标识
   header: Authorization
   # 令牌密钥
   secret: msbhjy
   # 令牌有效期(默认30分钟)
   expireTime: 30

创建TokenService

 /**
  * token验证处理
  * @author spikeCong
  * @date 2023/5/4
  **/
 public interface TokenService {
 
     /**
      * 创建令牌
      * @param loginUser
      * @return: java.lang.String
      */
     public String createToken(LoginUser loginUser);
 }
 /**
  * Token处理器
  * @author spikeCong
  * @date 2023/5/4
  **/
 public class TokenServiceImpl implements TokenService {
 
     // 令牌自定义标识
     @Value("${token.header}")
     private String header;
 
     // 令牌秘钥
     @Value("${token.secret}")
     private String secret;
 
     // 令牌有效期(默认30分钟)
     @Value("${token.expireTime}")
     private int expireTime;
 
     /**
      * 创建令牌
      * @param loginUser
      * @return: java.lang.String
      */
     @Override
     public String createToken(LoginUser loginUser) {
 
         //设置唯一用户标识
         String userKey = UUIDUtils.randomUUID();
         loginUser.setToken(userKey);
 
         Map<String, Object> claims = new HashMap<>();
         claims.put(Constants.LOGIN_USER_KEY, userKey);
 
         //创建token, 将用户唯一标识 通过setClaims方法 保存到token中
         String token = Jwts.builder()
                 .setClaims(claims)
                 .signWith(SignatureAlgorithm.HS512, secret).compact();
 
         return token;
     }
 }
(4) 创建SysLoginController
 /**
  * 登录验证
  * @author spikeCong
  * @date 2023/5/4
  **/
 @RestController
 public class SysLoginController {
 
     @Autowired
     private SysLoginService loginService;
 
     /**
      * 登录方法
      * @param loginBody
      * @return: com.msb.hjycommunity.common.utils.ChainedMap
      */
     @PostMapping("/login")
     public ChainedMap login(@RequestBody LoginBody loginBody){
 
         //生成令牌
         String token = loginService.login(loginBody.getUsername(), loginBody.getPassword(),
                 loginBody.getCode(), loginBody.getUuid());
         return ChainedMap.create().set("token",token);
     }
 }
(5) 设计业务异常体系
  • 创建业务异常基类
 /**
  * 业务异常
  * @author spikeCong
  * @date 2023/5/5
  **/
 public class CustomException extends RuntimeException {
 
     /**
      * 状态码
      */
     private int code;
 
     /**
      * 是否成功
      */
     private boolean success;
 
     /**
      * 承载数据
      */
     private T data;
 
     /**
      * 返回消息
      */
     private String msg;
 
     public CustomException() {
     }
 
     public CustomException(String msg,int code) {
         this.code = code;
         this.msg = msg;
         this.success = HttpServletResponse.SC_OK == code;
     }
 
     public CustomException(int code, boolean success, T data, String msg) {
         this.code = code;
         this.success = success;
         this.data = data;
         this.msg = msg;
     }
 
     public int getCode() {
         return code;
     }
 
     public void setCode(int code) {
         this.code = code;
     }
 
     public boolean isSuccess() {
         return success;
     }
 
     public void setSuccess(boolean success) {
         this.success = success;
     }
 
     public T getData() {
         return data;
     }
 
     public void setData(T data) {
         this.data = data;
     }
 
     public String getMsg() {
         return msg;
     }
 
     public void setMsg(String msg) {
         this.msg = msg;
     }
 }
  • 验证码异常继承 CustomException
 /**
  * 验证码异常
  * @author spikeCong
  * @date 2023/5/4
  **/
 public class CaptchaNotMatchException extends CustomException {
 
     public CaptchaNotMatchException() {
         super("验证码错误",400);
     }
 }
  • BaseResponse 响应结果类中,添加一个接收三个参数的构造方法
 /**
  * 响应结果封装对象
  * @author spikeCong
  * @date 2023/2/28
  **/
 public class BaseResponse<T> implements Serializable {
 
     private static final long serialVersionUID = 1L;
 
     /**
      * 响应状态码
      */
     private String code;
 
     /**
      * 响应结果描述
      */
     private String msg; 
 
     /**
      * 返回的数据
      */
     private T data;
 
     /**
      * 是否成功
      */
     private boolean success;
 
 
     /**
      * 失败返回 三个参数·
      * @param code
      * @param message
      * @return: com.msb.hjycommunity.common.core.domain.BaseResponse<T>
      */
     public static <T> BaseResponse<T> fail(String code,String message,boolean success){
         BaseResponse<T> response = new BaseResponse<>();
         response.setCode(code);
         response.setMsg(message);
         response.setSuccess(success);
         return response;
     }
 }
  • 在全局异常类 GlobalExceptionHandler中添加业务异常
 /**
  * 全局异常处理器
  * @author spikeCong
  * @date 2023/2/28
  **/
 @RestControllerAdvice(annotations = RestController.class)
 public class GlobalExceptionHandler {
 
     /**
      * 业务异常
      */
     @ExceptionHandler(CustomException.class)
     public BaseResponse businessException(CustomException e) {
         if(Objects.isNull(e.getCode())){
             return BaseResponse.fail(e.getMsg());
         }
         return BaseResponse.fail(e.getCode()+"", e.getMsg(),e.isSuccess());
     }
 }
(6) 根据接口文档进行测试

获取验证码: http://localhost:9999/hejiayun/captchaImage

image.png

查看redis中的验证码

image.png

访问登录接口,携带 用户名,密码,验证码,UUID: http://localhost:9999/hejiayun/login

image.png

转换一下token,查看载荷信息

image.png

4.3.2.3 TokenService功能设计
(1) LoginUser添加新的属性
 // com.msb.hjycommunity.system.domain.LoginUser
 /**
  * 登录用户 身份权限对象
  * @author spikeCong
  * @date 2023/5/3
  **/
 public class LoginUser implements UserDetails {
 
     /**
      * 用户唯一标识
      */
     private String token;
 
     /**
      * 用户信息
      */
     private SysUser user;
 
     /**
      * 登录时间
      */
     private Long loginTime;
 
     /**
      * 过期时间
      */
     private Long expireTime;
 
     /**
      * 权限列表
      */
     private Set<String> permissions;
 
     //一定要有空参构造,否则序列化会失败
     public LoginUser() {
     }
 
     public LoginUser(SysUser user, Set<String> permissions) {
         this.user = user;
         this.permissions = permissions;
     }
     
 }
(2) TokenService刷新令牌有效期&缓存用户信息

在createToken方法中创建令牌时,要刷新Token

 public void refreshToken(LoginUser loginUser);
 //com.msb.hjycommunity.system.service.impl.TokenServiceImpl
 
 @Component
 public class TokenServiceImpl implements TokenService {
 
     @Autowired
     private RedisCache redisCache;
 
     // 令牌自定义标识
     @Value("${token.header}")
     private String header;
 
     // 令牌秘钥
     @Value("${token.secret}")
     private String secret;
 
     // 令牌有效期(默认30分钟)
     @Value("${token.expireTime}")
     private int expireTime;
 
     //毫秒
     private static final long MILLIS_SECOND = 1000;
 
     //分钟
     private static final long MILLIS_MINUTE = 60 * MILLIS_SECOND;
 
     //20分钟
     private static final Long MILLIS_MINUTE_TEN = 20 * 60 * 1000L;
 
     /**
      * 创建令牌
      * @param loginUser
      * @return: java.lang.String
      */
     @Override
     public String createToken(LoginUser loginUser) {
 
         //设置唯一用户标识
         String userKey = UUIDUtils.randomUUID();
         loginUser.setToken(userKey);
 
         //todo 刷新令牌保存用户信息
         refreshToken(loginUser);
 
         Map<String, Object> claims = new HashMap<>();
         claims.put(Constants.LOGIN_USER_KEY, userKey);
 
         //创建token, 将用户唯一标识 通过setClaims方法 保存到token中
         String token = Jwts.builder()
                 .setClaims(claims)
                 .signWith(SignatureAlgorithm.HS512, secret).compact();
 
         return token;
     }
 
     /**
      * 缓存用户信息&刷新令牌有效期
      * @param loginUser
      */
     @Override
     public void refreshToken(LoginUser loginUser) {
         loginUser.setLoginTime(System.currentTimeMillis());
         //过期时间30分钟
         loginUser.setExpireTime(loginUser.getLoginTime() + expireTime * MILLIS_MINUTE );
         // 根据uuid将loginUser缓存
         String userKey = getTokenKey(loginUser.getToken());
         redisCache.setCacheObject(userKey,loginUser,expireTime, TimeUnit.MINUTES);
     }
 
     //拼接tokenkey
     private String getTokenKey(String uuid) {
         return Constants.LOGIN_TOKEN_KEY + uuid;
     }
 }
(3) 获取请求中的token

w3c规定,请求头 Authorization用于验证用户身份。token应该写在请求头 Authorization中.

jwt token的标准写法 Authorization: Bearer aaa.bbb.ccc。 (bearer: 持票人)

 /**
      * 从request的请求头中 获取token
      * @param request 
      * @return: java.lang.String
      */
 private String getToken(HttpServletRequest request){
 
     String token = request.getHeader(this.header);
     if(!StringUtils.isEmpty(token) && token.startsWith(Constants.TOKEN_PREFIX)){
         token = token.replace(Constants.TOKEN_PREFIX,"");
     }
 
     return token;
 }
(4) 获取用户身份信息
  LoginUser getLoginUser(HttpServletRequest request);
     /**
      * 从Redis获取用户身份信息
      * @param request
      * @return: com.msb.hjycommunity.system.domain.LoginUser
      */
     @Override
     public LoginUser getLoginUser(HttpServletRequest request) {
 
         //获取请求携带的token
         String token = getToken(request);
         if(!StringUtils.isEmpty(token)){
             Claims claims = parseToken(token);
             //解析对应的用户信息和权限信息
             String uuid =(String) claims.get(Constants.LOGIN_USER_KEY);
             String userKey = getTokenKey(uuid);
             LoginUser loginUser = redisCache.getCacheObject(userKey);
             return loginUser;
         }
 
         return null;
     }
 
     /**
      * 从令牌中获取数据声明
      *
      * @param token 令牌
      * @return 数据声明
      */
     private Claims parseToken(String token)
     {
         return Jwts.parser()
                 .setSigningKey(secret)
                 .parseClaimsJws(token)
                 .getBody();
     }
 
(5) 验证令牌有效期
 public void verifyToken(LoginUser loginUser);
 /**
      * 验证令牌有效期,相差不足20分钟,自动刷新缓存
      * @param loginUser
      */
 @Override
 public void verifyToken(LoginUser loginUser){
     Long expireTime = loginUser.getExpireTime();
     long currentTimeMillis = System.currentTimeMillis();
     if(expireTime - currentTimeMillis <= MILLIS_MINUTE_TEN){
         refreshToken(loginUser);
     }
 }
(6) 设置用户与删除用户
     public void setLoginUser(LoginUser loginUser);
 
     public void delLoginUser(String token);
     /**
      * 设置用户身份信息
      */
     @Override
     public void setLoginUser(LoginUser loginUser){
         if(!Objects.isNull(loginUser) && !StringUtils.isEmpty(loginUser.getToken())){
             refreshToken(loginUser);
         }
     }
 
     /**
      * 删除用户身份信息
      */
     @Override
     public void delLoginUser(String token){
         if(!StringUtils.isEmpty(token)){
             String userKey = getTokenKey(token);
             redisCache.deleteObject(userKey);
         }
     }
4.3.2.4 实现认证过滤器
(1) 创建JwtAuthenticationTokenFilter

当用户再次发送请求的时候,要进行校验,用户会携带登录时生成的JWT,所以我们需要自定义一个Jwt认证过滤器

image.png

  • 获取token
  • 解析token获取其中的用户唯一标识
  • 从redis中获取用户信息
  • 存入SecurityContextHolder

自定义一个过滤器,这个过滤器会去获取请求头中的token,对token进行解析取出其中的userid

 /**
  * token过滤器 验证token有效性
  * @author spikeCong
  * @date 2023/5/6
  **/
 @Component
 public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
 
     @Autowired
     private TokenService tokenService;
 
     @Override
     protected void doFilterInternal(HttpServletRequest request,
                                     HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
 
         //从Redis获取用户信息
         LoginUser loginUser = tokenService.getLoginUser(request);
         Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
 
         //判断: loginUser不为空,authentication为空,用户持有token 需要验证
         if(!Objects.isNull(loginUser) && Objects.isNull(authentication)){
             tokenService.verifyToken(loginUser);
        
             UsernamePasswordAuthenticationToken authenticationToken =
                     new UsernamePasswordAuthenticationToken(loginUser,null,loginUser.getAuthorities());
             //设置与当前身份验证相关的详细信息(远程IP地址、会话ID等)
             authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
             SecurityContextHolder.getContext().setAuthentication(authenticationToken);
 
         }
 
         filterChain.doFilter(request,response);
     }
 }
(2) 解决跨域问题
 /**
  * 通用配置
  * @author spikeCong
  * @date 2023/5/7
  **/
 @Configuration
 public class ResourcesConfig {
 
     /**
      * 跨域配置
      */
     @Bean
     public CorsFilter corsFilter()
     {
         UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
 
         CorsConfiguration config = new CorsConfiguration();
         config.setAllowCredentials(true);
         // 设置访问源地址
         config.addAllowedOrigin("*");
         // 设置访问源请求头
         config.addAllowedHeader("*");
         // 设置访问源请求方法
         config.addAllowedMethod("*");
         // 对接口配置跨域设置
         source.registerCorsConfiguration("/**", config);
         return new CorsFilter(source);
     }
 }
(3) 配置SecurityConfig
 /**
  * Security配置
  * @author spikeCong
  * @date 2023/5/3
  **/
 @EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
 public class SecurityConfig extends WebSecurityConfigurerAdapter {
 
     /**
      * 认证失败处理器
      */
     @Autowired
     private AuthenticationEntryPoint unauthorizedHandler;
 
     @Autowired
     private JwtAuthenticationTokenFilter authenticationTokenFilter;
 
     @Autowired
     private CorsFilter corsFilter;
 
     /**
      * 解决 无法直接注入 AuthenticationManager
      */
     @Bean
     @Override
     public AuthenticationManager authenticationManagerBean() throws Exception
     {
         return super.authenticationManagerBean();
     }
 
 
     @Override
     protected void configure(HttpSecurity http) throws Exception {
 
         http
                 // CSRF禁用,因为不使用session
                 .csrf().disable().sessionManagement()
                 //基于token,所以不需要session
                 .sessionCreationPolicy(SessionCreationPolicy.STATELESS);
 
         http
                 //过滤请求
                 .authorizeRequests()
                 // 对于登录login 验证码captchaImage 允许匿名访问
                 .mvcMatchers("/login","/captchaImage").anonymous()
                 // 除上面外的所有请求全部需要鉴权认证
                 .anyRequest().authenticated();
         http
                 //认证失败处理器
                 .exceptionHandling().authenticationEntryPoint(unauthorizedHandler);
 
         //添加JWTFilter
         http.addFilterBefore(authenticationTokenFilter,
                 UsernamePasswordAuthenticationFilter.class);
 
         //添加CORS filter
         http.addFilterBefore(corsFilter, JwtAuthenticationTokenFilter.class);
         //确保在用户注销登录时,响应头中包含必要的跨域资源共享(CORS)字段
         http.addFilterBefore(corsFilter, LogoutFilter.class);
     }
 
     /*
      * 配置密码加密方式
      */
     @Bean
     public PasswordEncoder passwordEncoder(){
         return new BCryptPasswordEncoder();
     }
 }
(4) 测试
  1. 获取验证码

image.png

image.png

  1. 发送登录请求,携带验证码

image.png

  1. 未携带token,查询小区数据

image.png

  1. 请求头中携带token访问

image.png

4.3.3 获取用户权限信息接口

image.png

4.3.3.1 创建角色与菜单实体类
(1) 创建角色实体类
 /**
  * 角色表 sys_role
  * @author spikeCong
  * @date 2023/5/9
  **/
 public class SysRole extends BaseEntity {
 
     private static final long serialVersionUID = 1L;
 
     /** 角色ID */
     @Excel(name = "角色序号")
     @TableId
     private Long roleId;
 
     /** 角色名称 */
     @Excel(name = "角色名称")
     private String roleName;
 
     /** 角色权限 */
     @Excel(name = "角色权限")
     private String roleKey;
 
     /** 角色排序 */
     @Excel(name = "角色排序")
     private String roleSort;
 
     /** 数据范围(1:所有数据权限;2:自定义数据权限;3:本部门数据权限;4:本部门及以下数据权限) */
     @Excel(name = "数据范围", replace = {"所有数据权限_1","自定义数据权限_2,","本部门数据权限_3","本部门及以下数据权限_4"})
     private String dataScope;
 
     /** 菜单树选择项是否关联显示( 0:父子不互相关联显示 1:父子互相关联显示) */
     private boolean menuCheckStrictly;
 
     /** 部门树选择项是否关联显示(0:父子不互相关联显示 1:父子互相关联显示 ) */
     private boolean deptCheckStrictly;
 
     /** 角色状态(0正常 1停用) */
     @Excel(name = "角色状态",replace = {"正常_0","停用_1"})
     private String status;
 
     /** 删除标志(0代表存在 2代表删除) */
     private String delFlag;
 
     /** 用户是否存在此角色标识 默认不存在 */
     private boolean flag = false;
 
     /** 菜单组 */
     private Long[] menuIds;
 
     /** 部门组(数据权限) */
     private Long[] deptIds;
 
     //判断是否是admin
     public boolean isAdmin()
     {
         return isAdmin(this.roleId);
     }
 
     public static boolean isAdmin(Long roleId)
     {
         return roleId != null && 1L == roleId;
     }
     
 }
(2) 创建菜单实体类
 /**
  * 菜单权限表 sys_menu
  * @author spikeCong
  * @date 2023/5/9
  **/
 public class SysMenu extends BaseEntity {
 
     private static final long serialVersionUID = 1L;
 
     /** 菜单ID */
     @TableId
     private Long menuId;
 
     /** 菜单名称 */
     private String menuName;
 
     /** 父菜单名称 */
     private String parentName;
 
     /** 父菜单ID */
     private Long parentId;
 
     /** 显示顺序 */
     private String orderNum;
 
     /** 路由地址 */
     private String path;
 
     /** 组件路径 */
     private String component;
 
     /** 是否为外链(0是 1否) */
     private String isFrame;
 
     /** 是否缓存(0缓存 1不缓存) */
     private String isCache;
 
     /** 类型(M目录 C菜单 F按钮) */
     private String menuType;
 
     /** 显示状态(0显示 1隐藏) */
     private String visible;
 
     /** 菜单状态(0显示 1隐藏) */
     private String status;
 
     /** 权限字符串 */
     private String perms;
 
     /** 菜单图标 */
     private String icon;
 
     /** 子菜单 */
     private List<SysMenu> children = new ArrayList<SysMenu>();
     
 }
4.3.3.2 根据用户ID获取角色权限信息
(1) 创建SysRoleMapper
 /**
  * 角色表 数据层
  * @author spikeCong
  * @date 2023/5/9
  **/
 public interface SysRoleMapper extends BaseMapper<SysRole> {
 
 
     /**
      * 根据用户ID 查询角色
      * @param userId
      * @return: 角色列表
      */
     public List<String> selectRolePermissionByUserId(Long userId);
 }
 <?xml version="1.0" encoding="UTF-8" ?>
 <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
 
 <mapper namespace="com.msb.hjycommunity.system.mapper.SysRoleMapper">
 
     <select id="selectRolePermissionByUserId" parameterType="Long" resultType="String">
         SELECT DISTINCT
             DISTINCT r.role_key
         FROM sys_role r
             LEFT JOIN sys_user_role ur ON ur.role_id = r.role_id
             LEFT JOIN sys_user u ON u.user_id = ur.user_id
         WHERE r.del_flag = '0' AND ur.user_id = #{userId}
     </select>
 </mapper>
(2) 创建SysRoleService
 /**
  * 角色业务层
  * @author spikeCong
  * @date 2023/5/9
  **/
 public interface SysRoleService {
 
     /**
      * 根据用户ID查询角色信息
      * @param userId
      * @return: 角色权限列表
      */
     public Set<String> selectRolePermissionByUserId(Long userId);
 
 }
 
 /**
  * 角色业务处理层
  * @author spikeCong
  * @date 2023/5/9
  **/
 @Service
 public class SysRoleServiceImpl implements SysRoleService {
 
     @Autowired
     private SysRoleMapper sysRoleMapper;
 
     /**
      * 根据用户ID查询角色信息
      * @param userId
      * @return: 角色权限列表
      */
     @Override
     public Set<String> selectRolePermissionByUserId(Long userId) {
 
         //根据用户Id获取角色信息
         List<String> roleList = sysRoleMapper.selectRolePermissionByUserId(userId);
 
         //将角色信息List集合转换为Set集合
         Set<String> permsSet = new HashSet<>();
         for (String roleKey : roleList) {
             if(!StringUtils.isEmpty(roleKey)){
                 permsSet.add(roleKey);
             }
         }
         return permsSet;
     }
 }
4.3.3.3 根据用户ID获取菜单权限信息
(1) 创建SysMenuMapper
 /**
  * 菜单表 数据层
  * @author spikeCong
  * @date 2023/5/9
  **/
 public interface SysMenuMapper extends BaseMapper<SysMenu> {
 
     /**
      * 根据用户ID查询权限
      *
      * @param userId 用户ID
      * @return 权限列表
      */
     public List<String> selectMenuPermsByUserId(Long userId);
 }
 <?xml version="1.0" encoding="UTF-8" ?>
 <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
 
 <mapper namespace="com.msb.hjycommunity.system.mapper.SysMenuMapper">
 
     <select id="selectMenuPermsByUserId" parameterType="Long" resultType="String">
 select
     distinct m.perms
 from sys_menu m
  left join sys_role_menu rm on m.menu_id = rm.menu_id
  left join sys_user_role ur on rm.role_id = ur.role_id
  left join sys_role r on r.role_id = ur.role_id
 where m.status = '0' and r.status = '0' and ur.user_id = #{userId}
 </select>
 
 </mapper>
(2) 创建SysMenuService
 /**
  * 菜单业务层
  * @author spikeCong
  * @date 2023/5/9
  **/
 public interface SysMenuService {
 
     /**
      * 根据用户Id查询用户权限
      * @param userId
      * @return: java.util.Set<java.lang.String>
      */
     public Set<String> selectMenuPermsByUserId(Long userId);
 }
 
 /**
  * @author spikeCong
  * @date 2023/5/9
  **/
 @Service
 public class SysMenuServiceImpl implements SysMenuService {
 
     @Autowired
     private SysMenuMapper menuMapper;
 
     @Override
     public Set<String> selectMenuPermsByUserId(Long userId) {
         List<String> menuList = menuMapper.selectMenuPermsByUserId(userId);
         Set<String> permsSet = new HashSet<>();
         for (String menu : menuList) {
             if(!StringUtils.isEmpty(menu)){
                 permsSet.add(menu);
             }
         }
         return permsSet;
     }
 }
4.3.3.4 根据用户名获取完整用户信息
(1) 修改SysUser

通过查看接口文档可以发现,返回的用户信息中,要求包含 :

  • dept 部门对象
  • roles 角色对象集合
  • roleIds 角色组
  • postIds 岗位组
 /**
  * 用户表 sys_user
  * @author spikeCong
  * @date 2023/5/3
  **/
 public class SysUser extends BaseEntity
 {
     private static final long serialVersionUID = 1L;
 
     /** 用户ID */
     @Excel(name = "用户序号")
     @TableId
     private Long userId;
 
     /** 部门ID */
     @Excel(name = "部门编号")
     private Long deptId;
 
     /** 用户账号 */
     @Excel(name = "登录名称")
     private String userName;
 
     /** 用户昵称 */
     @Excel(name = "用户名称")
     private String nickName;
 
     /** 用户邮箱 */
     @Excel(name = "用户邮箱")
     private String email;
 
     /** 手机号码 */
     @Excel(name = "手机号码")
     private String phonenumber;
 
     /** 用户性别 */
     @Excel(name="用户性别",replace = {"男_0","女_1","未知_0"})
     private String sex;
 
     /** 用户头像 */
     private String avatar;
 
     /** 密码 */
     private String password;
 
     /** 盐加密 */
     private String salt;
 
     /** 帐号状态(0正常 1停用) */
     @Excel(name = "帐号状态",replace = {"正常_0","停用_1"})
     private String status;
 
     /** 删除标志(0代表存在 2代表删除) */
     private String delFlag;
 
     /** 最后登录IP */
     @Excel(name = "最后登录IP")
     private String loginIp;
 
     /** 最后登录时间 */
     @Excel(name = "最后登录时间", width = 30, format = "yyyy-MM-dd HH:mm:ss")
     private Date loginDate;
 
     /** 部门对象 */
     private SysDept dept;
 
     /** 角色对象 */
     private List<SysRole> roles;
 
     /** 角色组 */
     private Long[] roleIds;
 
     /** 岗位组 */
     private Long[] postIds;
 
     //判断当前用户是否是admin 
     public boolean isAdmin()
     {
         return isAdmin(this.userId);
     }
 
     public static boolean isAdmin(Long userId)
     {
         return userId != null && 1L == userId;
     }
 }
(2) 修改SysUserMapper中的selectUserByUserName方法
  • **根据用户名查询用户信息的方法是在 **UserDetailsServiceImpl 中,调用的SysUserMapper中的 selectUserByUserName方法, 所以需要获取更加详细的用户信息的话,修改XML即可
 <?xml version="1.0" encoding="UTF-8" ?>
 <!DOCTYPE mapper
         PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
         "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
 <mapper namespace="com.msb.hjycommunity.system.mapper.SysUserMapper">
 
     <resultMap type="SysUser" id="SysUserResult">
         <id     property="userId"       column="user_id"      />
         <result property="deptId"       column="dept_id"      />
         <result property="userName"     column="user_name"    />
         <result property="nickName"     column="nick_name"    />
         <result property="email"        column="email"        />
         <result property="phonenumber"  column="phonenumber"  />
         <result property="sex"          column="sex"          />
         <result property="avatar"       column="avatar"       />
         <result property="password"     column="password"     />
         <result property="status"       column="status"       />
         <result property="delFlag"      column="del_flag"     />
         <result property="loginIp"      column="login_ip"     />
         <result property="loginDate"    column="login_date"   />
         <result property="createBy"     column="create_by"    />
         <result property="createTime"   column="create_time"  />
         <result property="updateBy"     column="update_by"    />
         <result property="updateTime"   column="update_time"  />
         <result property="remark"       column="remark"       />
         <association property="dept"    column="dept_id" javaType="SysDept" resultMap="deptResult" />
         <collection  property="roles"   javaType="java.util.List"        resultMap="RoleResult" />
     </resultMap>
 
     <resultMap id="deptResult" type="SysDept">
         <id     property="deptId"   column="dept_id"     />
         <result property="parentId" column="parent_id"   />
         <result property="deptName" column="dept_name"   />
         <result property="orderNum" column="order_num"   />
         <result property="leader"   column="leader"      />
         <result property="status"   column="dept_status" />
     </resultMap>
 
     <resultMap id="RoleResult" type="SysRole">
         <id     property="roleId"       column="role_id"        />
         <result property="roleName"     column="role_name"      />
         <result property="roleKey"      column="role_key"       />
         <result property="roleSort"     column="role_sort"      />
         <result property="dataScope"     column="data_scope"    />
         <result property="status"       column="role_status"    />
     </resultMap>
 
     <sql id="selectUserVo">
         SELECT
             u.user_id, u.dept_id, u.user_name,
             u.nick_name, u.email, u.avatar, u.phonenumber,
             u.password, u.sex, u.status, u.del_flag, u.login_ip,
             u.login_date, u.create_by, u.create_time, u.remark,
             d.dept_id, d.parent_id, d.dept_name, d.order_num,
             d.leader, d.status AS dept_status,
             r.role_id, r.role_name, r.role_key, r.role_sort,
             r.data_scope, r.status AS role_status
         FROM sys_user u
             LEFT JOIN sys_dept d ON u.dept_id = d.dept_id
             LEFT JOIN sys_user_role ur ON u.user_id = ur.user_id
             LEFT JOIN sys_role r ON r.role_id = ur.role_id
     </sql>
 
     <select id="selectUserByUserName" parameterType="string" resultMap="SysUserResult">
         <include refid="selectUserVo"/>
         where u.user_name = #{userName}
     </select>
 
 </mapper>
(3) 测试查询完整用户数据
 @Test
 public void testSelectUserByUserName(){
     SysUser admin = userMapper.selectUserByUserName("admin");
     System.out.println(admin);
 }
4.3.3.5 用户信息接口编写
(1) 创建用户权限处理Service
  • 创建一个专门用于获取用户权限的service,让controller直接调用
 //com.msb.hjycommunity.framework.service.SysPermissionService
 /**
  * 用户权限处理
  * @author spikeCong
  * @date 2023/5/9
  **/
 @Component
 public class SysPermissionService {
 
     @Autowired
     private SysRoleService roleService;
 
     @Autowired
     private SysMenuService menuService;
 
     /**
      * 获取角色数据权限
      * @param user
      * @return: 角色权限信息
      */
     public Set<String> getRolePermission(SysUser user){
 
         Set<String> roles = new HashSet<>();
         //管理员拥有所有权限
         if(user.isAdmin()){
             roles.add("admin");
         }else{
             roles = roleService.selectRolePermissionByUserId(user.getUserId());
         }
 
         return roles;
     }
 
     /**
      * 获取菜单数据权限
      * @param user
      * @return: java.util.Set<java.lang.String>
      */
     public Set<String> getMenuPermission(SysUser user){
         Set<String> perms = new HashSet<>();
         //管理员拥有所有权限
         if(user.isAdmin()){
             perms.add("*:*:*");
         }else{
             perms = menuService.selectMenuPermsByUserId(user.getUserId());
         }
         return perms;
     }
 }
(2) SysLoginController编写获取用户信息方法
 
 /**
  * 登录验证
  * @author spikeCong
  * @date 2023/5/4
  **/
 @RestController
 public class SysLoginController {
 
     @Autowired
     private SysLoginService loginService;
 
     @Autowired
     private SysPermissionService permissionService;
 
     @Autowired
     private TokenService tokenService;
 
     /**
      * 获取 用户信息
      * @param
      * @return: 用户信息
      */
     @GetMapping("/getInfo")
     public ChainedMap getInfo(){
 
         //用户信息
         LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
         SysUser user = loginUser.getUser();
         //角色集合
         Set<String> roles = permissionService.getRolePermission(user);
         //权限集合
         Set<String> permissions = permissionService.getMenuPermission(user);
         
         ChainedMap map = ChainedMap.create().set("code", 200).set("msg", "操作成功");
         map.put("user",user);
         map.put("roles",roles);
         map.put("permissions",permissions);
         return map;
     }
 }
(3) 根据接口文档进行测试

4.3.4 获取路由导航菜单信息

在首页加载时,前端会向后端发送请求,获取左侧导航菜单及其子菜单数据.

image.png

4.3.4.1 SysMenuMapper
(1) 查询所有菜单
 /**
  * 菜单表 数据层
  * @author spikeCong
  * @date 2023/5/9
  **/
 public interface SysMenuMapper extends BaseMapper<SysMenu> {
     /**
      * 用户为admin时,查询全部菜单信息
      * @param
      * @return: 菜单列表
      */
     public List<SysMenu> selectMenuTreeAll();
 }
 <mapper namespace="com.msb.hjycommunity.system.mapper.SysMenuMapper">
 
 <resultMap type="SysMenu" id="SysMenuResult">
 <id     property="menuId"         column="menu_id"        />
 <result property="menuName"       column="menu_name"      />
 <result property="parentName"     column="parent_name"    />
 <result property="parentId"       column="parent_id"      />
 <result property="orderNum"       column="order_num"      />
 <result property="path"           column="path"           />
 <result property="component"      column="component"      />
 <result property="isFrame"        column="is_frame"       />
 <result property="isCache"        column="is_cache"       />
 <result property="menuType"       column="menu_type"      />
 <result property="visible"        column="visible"        />
 <result property="status"         column="status"         />
 <result property="perms"          column="perms"          />
 <result property="icon"           column="icon"           />
 <result property="createBy"       column="create_by"      />
 <result property="createTime"     column="create_time"    />
 <result property="updateTime"     column="update_time"    />
 <result property="updateBy"       column="update_by"      />
 <result property="remark"         column="remark"         />
 </resultMap>
 
 <select id="selectMenuTreeAll" resultMap="SysMenuResult">
 SELECT
  DISTINCT sm.menu_id, sm.parent_id, sm.menu_name,
  sm.path, sm.component, sm.visible, sm.status, IFNULL(sm.perms,'') AS perms,
  sm.is_frame, sm.is_cache, sm.menu_type, sm.icon, sm.order_num, sm.create_time
 FROM sys_menu sm
 WHERE sm.menu_type IN ('M','C') AND sm.status = 0
 ORDER BY sm.parent_id,sm.order_num
 </select>
 </mapper>
(2) 根据用户ID查询菜单
 /**
      * 根据用户id 查询菜单信息
      * @param
      * @return: 菜单列表
      */
 public List<SysMenu> selectMenuTreeByUserId(Long userId);
 <select id="selectMenuTreeByUserId" parameterType="Long" resultMap="SysMenuResult">
     SELECT
         DISTINCT sm.menu_id, sm.parent_id, sm.menu_name, sm.path, sm.component,
         sm.visible, sm.status, IFNULL(sm.perms,'') AS perms, sm.is_frame,
         sm.is_cache, sm.menu_type, sm.icon, sm.order_num, sm.create_time
     FROM sys_menu sm
         LEFT JOIN sys_role_menu srm ON sm.menu_id = srm.menu_id
         LEFT JOIN sys_role sr ON srm.role_id = sr.role_id
         LEFT JOIN sys_user_role sur ON sr.role_id = sur.role_id
         LEFT JOIN sys_user su ON sur.user_id = su.user_id
     WHERE su.user_id = #{userId} and sm.menu_type in ('M','C')and sm.status = 0 and sr.status = 0
     ORDER BY sm.parent_id,sm.order_num;
 </select>
4.3.4.2 SysMenuService
(1) 根据用户ID查询菜单树信息
 /**
      * 根据用户ID 查询菜单树信息
      * @param userId
      * @return: 菜单列表
      */
 public List<SysMenu> selectMenuTreeByUserId(Long userId);
 @Override
 public List<SysMenu> selectMenuTreeByUserId(Long userId) {
 
     List<SysMenu> menus = null;
     if(userId != null && 1L == userId){
         menus = menuMapper.selectMenuTreeAll();
     }else{
         menus = menuMapper.selectMenuTreeByUserId(userId);
     }
 
     //todo 获取子菜单
      return getChildPerms(menus,0);
 }
(2) 根据父节点ID获取所有子节点
 /**
      * 根据父节点ID 获取所有子节点
      * @param menus
      * @param parentId 传入的父节点Id
      * @return: java.util.List<com.msb.hjycommunity.system.domain.SysMenu>
      */
 private List<SysMenu> getChildPerms(List<SysMenu> menus, int parentId) {
 
     List<SysMenu> returnList = new ArrayList<>();
     menus.stream()
         .filter(m-> m.getParentId() == parentId)
         .forEach(m -> {
             recursionFn(menus,m);
             returnList.add(m);
         });
 
     return returnList;
 }
 
 /**
      * 递归获取子菜单
      * @param menus
      * @param m
      */
 private void recursionFn(List<SysMenu> menus, SysMenu m) {
     //得到子节点列表,保存到父菜单的children中
     List<SysMenu> childList = getChildList(menus,m);
     m.setChildren(childList);
     for (SysMenu childMenu : childList) {
         //判断子节点下是否还有子节点
         if(getChildList(menus, childMenu).size() > 0 ? true : false){
             recursionFn(menus, childMenu);
         }
     }
 }
 
 /**
      * 得到子节点列表
      * @param menus
      * @param m
      * @return: 子菜单集合
      */
 private List<SysMenu> getChildList(List<SysMenu> menus, SysMenu m) {
     List<SysMenu> subMenus = menus.stream()
         .filter(sub -> sub.getParentId().longValue() == m.getMenuId().longValue())
         .collect(Collectors.toList());
 
     return subMenus;
 }
4.3.4.3 SysLoginController
(1) 实现获取路由信息功能
     /**
      * 获取路由信息
      * @param  
      * @return: 路由信息
      */
     @GetMapping("/getRouters")
     public BaseResponse getRouters(){
         LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
         SysUser user = loginUser.getUser();
         List<SysMenu> menus = sysMenuService.selectMenuTreeByUserId(user.getUserId());
         
         return BaseResponse.success(menus);
     }
(2) 根据接口文档进行测试

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

4.3.4.4 构建前端路由所需要的菜单

测试获取的JSON数据不符合接口文档要求

(1) 创建前端所需的菜单路由实体
  • 路由配置信息
 /**
  * 路由配置信息VO
  * @author spikeCong
  * @date 2023/5/11
  **/
 public class RouterVo {
 
     /**
      * 路由名字
      */
     private String name;
 
     /**
      * 路由地址
      */
     private String path;
 
     /**
      * 是否隐藏路由,当设置 true 的时候该路由不会再侧边栏出现
      */
     private boolean hidden;
 
     /**
      * 重定向地址,当设置 noRedirect 的时候该路由在面包屑导航中不可被点击
      */
     private String redirect;
 
     /**
      * 组件地址
      */
     private String component;
 
     /**
      * 当你一个路由下面的 children 声明的路由大于1个时,自动会变成嵌套的模式--如组件页面
      */
     private Boolean alwaysShow;
 
     /**
      * 其他元素
      */
     private MetaVo meta;
     
     /**
      * 子路由
      */
     private List<RouterVo> children;
 
 //get... set...
 }
  • 路由显示信息
 /**
  * 路由显示信息
  * @author spikeCong
  * @date 2023/5/11
  **/
 public class MetaVo {
 
     /**
      * 设置该路由在侧边栏和面包屑中展示的名字
      */
     private String title;
 
     /**
      * 设置该路由的图标,对应路径src/assets/icons/svg
      */
     private String icon;
 
     /**
      * 设置为true,则不会被 <keep-alive>缓存
      */
     private boolean noCache;
 
     public MetaVo()
     {
     }
 
     public MetaVo(String title, String icon)
     {
         this.title = title;
         this.icon = icon;
     }
 
     public MetaVo(String title, String icon, boolean noCache)
     {
         this.title = title;
         this.icon = icon;
         this.noCache = noCache;
     }
 
     public boolean isNoCache()
     {
         return noCache;
     }
 
     public void setNoCache(boolean noCache)
     {
         this.noCache = noCache;
     }
 
     public String getTitle()
     {
         return title;
     }
 
     public void setTitle(String title)
     {
         this.title = title;
     }
 
     public String getIcon()
     {
         return icon;
     }
 
     public void setIcon(String icon)
     {
         this.icon = icon;
     }
 }
  • 添加用户菜单常量类
 /**
  * 用户常量信息
  * @author spikeCong
  * @date 2023/5/11
  **/
 public class UserConstants {
 
     /**
      * 平台内系统用户的唯一标志
      */
     public static final String SYS_USER = "SYS_USER";
 
     /** 正常状态 */
     public static final String NORMAL = "0";
 
     /** 异常状态 */
     public static final String EXCEPTION = "1";
 
     /** 用户封禁状态 */
     public static final String USER_DISABLE = "1";
 
     /** 角色封禁状态 */
     public static final String ROLE_DISABLE = "1";
 
     /** 部门正常状态 */
     public static final String DEPT_NORMAL = "0";
 
     /** 部门停用状态 */
     public static final String DEPT_DISABLE = "1";
 
     /** 字典正常状态 */
     public static final String DICT_NORMAL = "0";
 
     /** 是否为系统默认(是) */
     public static final String YES = "Y";
 
     /** 是否菜单外链(是) */
     public static final String YES_FRAME = "0";
 
     /** 是否菜单外链(否) */
     public static final String NO_FRAME = "1";
 
     /** 菜单类型(目录) */
     public static final String TYPE_DIR = "M";
 
     /** 菜单类型(菜单) */
     public static final String TYPE_MENU = "C";
 
     /** 菜单类型(按钮) */
     public static final String TYPE_BUTTON = "F";
 
     /** Layout组件标识 */
     public final static String LAYOUT = "Layout";
 
     /** ParentView组件标识 */
     public final static String PARENT_VIEW = "ParentView";
 
     /** 校验返回结果码 */
     public final static String UNIQUE = "0";
     public final static String NOT_UNIQUE = "1";
 }
(2) 构建前端路由所需要的菜单

image.png

image.png

image.png

  • SysMenuService
 /**
      * 构建前端路由所需要的菜单
      * @param menus 菜单列表
      * @return: 路由列表
      */
 public List<RouterVo> buildMenus(List<SysMenu> menus);
  • SysMenuServiceImpl ==> 设置路由名称
     @Override
     public List<RouterVo> buildMenus(List<SysMenu> menus) {
 
         List<RouterVo> routers = new LinkedList<>();
         for (SysMenu menu : menus) {
             RouterVo routerVo = new RouterVo();
             routerVo.setName(getRouteName(menu));
 
         }
 
         return null;
     }
 
     /**
      * 获取路由名称
      * @param menu  菜单信息
      * @return: 路由名称
      */
     public String getRouteName(SysMenu menu) {
         String routerName = org.apache.commons.lang3.StringUtils.capitalize(menu.getPath());
         return routerName;
     }
  • SysMenuServiceImpl ==> 设置路由地址
 @Override
 public List<RouterVo> buildMenus(List<SysMenu> menus) {
 
     List<RouterVo> routers = new LinkedList<>();
     for (SysMenu menu : menus) {
         RouterVo routerVo = new RouterVo();
         routerVo.setName(getRouteName(menu));
         routerVo.setPath(getRoutePath(menu));
 
     }
 
     return null;
 }
 
 /**
      * 获取路由地址
      * @param menu 菜单信息
      * @return: 路由地址
      */
 public String getRoutePath(SysMenu menu) {
     String routerPath = menu.getPath();
     //非外链 并且是一级目录,菜单类型为 M(目录)
     if(0 == menu.getParentId().intValue() && UserConstants.TYPE_DIR.equals(menu.getMenuType())
        && UserConstants.NO_FRAME.equals(menu.getIsFrame())){
         routerPath = "/" + menu.getPath();
     }
 
     return routerPath;
 }
  • SysMenuServiceImpl ==> 设置组件信息
 @Override
 public List<RouterVo> buildMenus(List<SysMenu> menus) {
 
     List<RouterVo> routers = new LinkedList<>();
     for (SysMenu menu : menus) {
         RouterVo routerVo = new RouterVo();
         routerVo.setName(getRouteName(menu));
         routerVo.setPath(getRoutePath(menu));
         routerVo.setComponent(getComponent(menu));
 
     }
 
     return null;
 }
 
 /**
      * 获取组件信息
      * @param menu
      * @return: 组件信息
      */
 public String getComponent(SysMenu menu) {
     String component = UserConstants.LAYOUT;
     if(!StringUtils.isEmpty(menu.getComponent())){
         component = menu.getComponent();
     }else if(menu.getParentId().intValue() != 0 && UserConstants.TYPE_DIR.equals(menu.getMenuType())){
         component = UserConstants.PARENT_VIEW;
     }
 
     return component;
 }
  • SysMenuServiceImpl ==> 设置其他信息
 @Override
 public List<RouterVo> buildMenus(List<SysMenu> menus) {
 
     List<RouterVo> routers = new LinkedList<>();
     for (SysMenu menu : menus) {
         RouterVo routerVo = new RouterVo();
         //设置路由名称 例如: System 开头字母大写
         routerVo.setName(getRouteName(menu));
         //设置路由地址 例如: 根目录 /system , 二级目录 user
         routerVo.setPath(getRoutePath(menu));
         //设置组件地址 例如: system/user/index
         routerVo.setComponent(getComponent(menu));
         //设置是否隐藏 ,隐藏后侧边栏不会出现
         routerVo.setHidden("1".equals(menu.getVisible()));
         //基础元素
         routerVo.setMeta(new MetaVo(menu.getMenuName(),menu.getIcon(),"1".equals(menu.getIsCache())));
         //子菜单
         List<SysMenu> subMenus = menu.getChildren();
         //子菜单不为空 && 类型为M 菜单类型(目录 顶级父菜单)
         if(!subMenus.isEmpty() && subMenus.size() > 0 && UserConstants.TYPE_DIR.equals(menu.getMenuType())){
             routerVo.setAlwaysShow(true);   //下面有子路由
             routerVo.setRedirect("noRedirect"); //在导航栏中不可点击
             routerVo.setChildren(buildMenus(subMenus)); //递归设置子菜单
         }
 
         routers.add(routerVo);
     }
     return routers;
 }

(3) 修改SysLoginController

 /**
      * 获取路由信息
      * @param
      * @return: 路由信息
      */
 @GetMapping("/getRouters")
 public BaseResponse getRouters(){
     LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
     SysUser user = loginUser.getUser();
     //获取菜单列表
     List<SysMenu> menus = sysMenuService.selectMenuTreeByUserId(user.getUserId());
     //转换为前端需要的路由列表
     List<RouterVo> routerVoList = sysMenuService.buildMenus(menus);
 
     return BaseResponse.success(routerVoList);
 }

image.png

4.3.5 自定义权限校验规则

(1) 修改LoginUser

添加permissions属性和构造方法

 public class LoginUser implements UserDetails {
 
     /**
      * 用户信息
      */
     private SysUser user;
 
     /**
      * 权限列表
      */
     private Set<String> permissions;
 
     public LoginUser(SysUser user, Set<String> permissions) {
         this.user = user;
         this.permissions = permissions;
     }
 }
(2) 修改UserDetailsServiceImpl
 /**
  * 用户验证处理
  * @author spikeCong
  * @date 2023/5/3
  **/
 @Service
 @Slf4j
 public class UserDetailsServiceImpl implements UserDetailsService {
 
     @Autowired
     private SysUserService userService;
 
     @Autowired
     private SysPermissionService permissionService;
 
     @Override
     public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
 
         SysUser user = userService.selectUserByUserName(username);
 
         if(Objects.isNull(user)){
             log.info("登录用户:{} 不存在",username);
             throw new UsernameNotFoundException("登录用户: " + username + " 不存在");
         }
         else if(UserStatus.DELETED.getCode().equals(user.getDelFlag())){
             log.info("登录用户:{} 已被删除",username);
             throw new BaseException("对不起,您的账号: " + username + " 以被删除" );
         }
         else if(UserStatus.DISABLE.getCode().equals(user.getStatus())){
             log.info("登录用户:{} 已被停用",username);
             throw new BaseException("对不起,您的账号: " + username + " 以被停用" );
         }
 
         return createLoginUser(user);
     }
 
     public UserDetails createLoginUser(SysUser user) {
 
         return new LoginUser(user,permissionService.getMenuPermission(user));
     }
 }
(3) 自定义权限校验

主要有以下几种校验方式

  • 验证用户是否具备某权限
  • 验证用户是否具有以下任意一个权限
  • 判断用户是否拥有某个角色
  • 验证用户是否具有以下任意一个角色
/**
 * 自定义权限校验
 * @author spikeCong
 * @date 2023/5/9
 **/
@Component("pe")
public class PermsExpressionService {

    /** 所有权限的标识 */
    private static final String ALL_PERMISSION = "*:*:*";

    private static final String DELIMITERS = ",";
  
    @Autowired
    private TokenService tokenService;
  
    /**
     * 验证用户是否具备某权限
     * @param permission    权限字符串
     * @return: boolean     是否拥有权限
     */
    public boolean hasPerms(String permission){
        if(StringUtils.isEmpty(permission)){
            return false;
        }
  
        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
  
        if(Objects.isNull(loginUser) || CollectionUtils.isEmpty(loginUser.getPermissions())){
            return false;
        }
  
        return hasPermissions(loginUser.getPermissions(),permission);
    }

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

    /**
     * 验证用户是否具有以下任意一个权限
     * @param permissions 
     * @return: boolean
     */
    public boolean hasAnyPerms(String permissions){
        if(StringUtils.isEmpty(permissions)){
            return false;
        }

        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
        if(Objects.isNull(loginUser) || CollectionUtils.isEmpty(loginUser.getPermissions())){
            return false;
        }

        Set<String> authorities = loginUser.getPermissions();
        for (String permission : permissions.split(DELIMITERS)) {
            if(permission != null && hasPermissions(authorities,permission)){
                return true;
            }
        }
        return false;
    }

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


    /**
     * 判断用户是否具有以下任意一个角色
     * @param roles  角色字符串,多个角色用逗号分隔
     * @return: boolean
     */
    public boolean hasAnyRole(String roles){

        if(StringUtils.isEmpty(roles)){
            return false;
        }
        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
        if(Objects.isNull(loginUser) || CollectionUtils.isEmpty(loginUser.getUser().getRoles())){
            return false;
        }
        for (String role : roles.split(DELIMITERS)) {
            if(hasRole(role)){
                return true;
            }
        }
        return false;
    }
}
(4) 测试权限校验

第一步: 添加权限校验

  • 在获取部门列表接口上加一个权限校验 @PreAuthorize("@pe.hasPermi('system:dept:list')")
/**
     * 获取部门列表
     * @param sysDept
     * @return: com.msb.hjycommunity.common.core.domain.BaseResponse
     */
@PreAuthorize("@pe.hasPerms('system:dept:list')")
@GetMapping("/list")
public BaseResponse list(SysDept sysDept){

    List<SysDept> sysDepts = deptService.selectDeptList(sysDept);
    return BaseResponse.success(sysDepts);
}
  • 在查询小区的接口上加一个权限校验 @PreAuthorize("@pe.hasPermi('system:community:list')")
/**
     * 查询小区
     * @param hjyCommunity
     * @return: com.msb.hjycommunity.common.core.page.PageResult
     */
@GetMapping("/list")
@PreAuthorize("@pe.hasPerms('system:community:list')")
public PageResult list(HjyCommunity hjyCommunity){

    startPage();
    List<HjyCommunityDto> list = hjyCommunityService.selectHjyCommunityList(hjyCommunity);

    //响应数据
    return getData(list);
}

第二步: 使用 laoli 账号登录

image.png

第三步: 获取用户 laoli 拥有的权限信息, laoli只有查看小区信息的权限,没有查看部门信息的权限

image.png

第四步: 分别访问查询小区信息接口、查询部门信息接口

  • 查询小区信息,可以获取数据

image.png

  • 查询部门信息,被拦截 没有通过验证

image.png

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

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

相关文章

【数据结构】【C++】封装红黑树模拟实现map和set容器

【数据结构】&&【C】封装红黑树模拟实现map和set容器 一.红黑树的完成二.改造红黑树(泛型适配)三.封装map和set的接口四.实现红黑树迭代器(泛型适配)五.封装map和set的迭代器六.解决key不能修改问题七.实现map[]运算符重载 一.红黑树的完成 在上一篇红黑树的模拟实现中…

抖音、知乎、小红书的流量算法

目前我国网民规模已超过10亿&#xff0c;在这互联网时代&#xff0c;更是流量为王。各个平台里的每个视频、每张图片&#xff0c;背后都有着算法的身影&#xff0c;支配着所有人的流量。作为内容创作者及运营者来说&#xff0c;除了制作高质量的内容以外&#xff0c;也需要掌握…

五年全满分 | 求臻医学满分通过2023 NGSST-A 能力验证

近期&#xff0c;求臻医学以满分的优异成绩顺利通过了美国病理学家协会&#xff08;College of American Pathologists, CAP&#xff09;组织开展的NGSST-A 2023&#xff08;Next-Generation Sequencing Solid Tumor&#xff09;能力验证项目。至此&#xff0c;公司已连续五年满…

如何去除音乐中的人声,只留伴奏?

如何去除音乐中的人声&#xff0c;只留伴奏&#xff1f;看到很多小伙伴都有这种需求&#xff0c;今天给大家分享一个宝藏网站&#xff0c;简易操作&#xff0c;可以轻松提取伴奏&#xff0c;想学的一起来看看吧&#xff01; 音分轨——人声分离软件&#xff0c;这是一个用了人工…

Spring 6.0 新特性

文章目录 Spring的发展历史AOTGraalVMSpringBoot实战AOTRuntimeHints案例分析RuntimeHintsRegistrar SpringBoot中AOT核心代码 Spring的发展历史 AOT Spring 6.0的新特性Ahead of Time&#xff08;AOT&#xff09;编译是一种技术&#xff0c;可以提前将Spring应用程序编译成原…

Linux为什么不能像鸿蒙一样实现万物互联?

Linux为什么不能像鸿蒙一样实现万物互联? 可能原因&#xff0c;Linux不会炒作。 万物互联&#xff0c;先从网络开始&#xff0c;光纤入户&#xff0c;首先接入光猫&#xff0c;光猫的操作系统&#xff0c;不确定是不是openWRT&#xff0c;但是这个确定是Linux内核。 然后进入…

免杀对抗-java语言-shellcode免杀-源码修改+打包exe

JAVA-ShellCode免杀-源码修改&打包EXE Shellcode-生成/上线 1.msf生成shellcode 命令&#xff1a;msfvenom -p java/meterpreter/reverse_tcp LHOSTx.x.x.x LPORTxxxx -f jar -o msf.jar 2.msf设置监听 3.执行msf生成的shellcode jar包&#xff0c;成功上线 命令&#xff1…

【再识C进阶2(下)】详细介绍指针的进阶——利用冒泡排序算法模拟实现qsort函数,以及一下习题和指针笔试题

前言 &#x1f493;作者简介&#xff1a; 加油&#xff0c;旭杏&#xff0c;目前大二&#xff0c;正在学习C&#xff0c;数据结构等&#x1f440; &#x1f493;作者主页&#xff1a;加油&#xff0c;旭杏的主页&#x1f440; ⏩本文收录在&#xff1a;再识C进阶的专栏&#x1…

echarts柱状图上方的数值为0时设置不展示

未去掉0的效果图 series: [{data: this.barData.y,type: "bar",barWidth: 20,itemStyle: {normal: {// 设置柱子圆角barBorderRadius: [2, 2, 0, 0],},},label: {show: true,position: top,color:#fff,formatter: function (params) {if (params.value > 0) {retu…

Vue3父组件访问子组件方法 defineExpose用法

父组件示例 <template><div class"container"><h-demo ref"childDom" /><button click"getChild"></button></div> </template><script setup> import Hdemo from /components/Hdemo; import …

利用vue-cli构建SPA项目以及在SPA项目中使用路由

一. 利用vue-cli构建SPA项目 构建前提&#xff1a; Node.js环境已经搭建完成&#xff08;可点击上一篇博客进行查看http://t.csdn.cn/i2Rg5&#xff09; 1.1 vue-cli是什么 vue-cli是一个用于快速搭建Vue.js项目的脚手架工具。用于自动生成vue.jswebpack的项目模板。它提供了一…

无涯教程-JavaScript - COUNT函数

描述 COUNT函数计算包含数字的单元格的数量,并计算参数列表中的数字。使用COUNT函数获取在数字范围或数字数组中的数字字段中的条目数。 语法 COUNT (value1, [value2] ...)争论 Argument描述Required/Optionalvalue1The first item, cell reference, or range within whic…

MeterSphere v2.10.X-lts 双节点HA部署方案

一、MeterSphere高可用部署架构及服务器配置 1.1 服务器信息 序号应用名称操作系统要求配置要求描述1负载均衡器CentOS 7.X /RedHat 7.X2C,4G&#xff0c;200GB部署Nginx&#xff0c;实现负载路由。 部署NFS服务器。2MeterSphere应用节点1CentOS 7.X /RedHat 7.X8C,16GB,200G…

b站老王 自动驾驶决策规划学习记录(十二)

自动驾驶之速度规划详解&#xff1a;SL与ST迭代 上一讲&#xff1a;b站老王 自动驾驶决策规划学习记录&#xff08;十一&#xff09; 接着上一讲学习记录b站老王对自动驾驶规划系列的讲解 参考视频&#xff1a; 自动驾驶决策规划算法第二章第七节(上) 速度规划详解:SL与ST迭代…

ubuntu 22.04 服务器网卡无IP地址

ssh连接服务器连接不上&#xff0c;提示如下&#xff1b; 连接显示器&#xff0c;ip addr ls 命令查看IP地址&#xff0c;有网卡但没有IP地址 solution&#xff1a; sudo dhclient enp10s0用于通过 DHCP 协议获取网络配置信息并为名为 enp10s0 的网络接口分配 IP 地址,enp1…

学会根据数据手册指令格式发送数据

根据教导&#xff0c;我才知道如何发送指令 EF AA 00 00 00 00 02 C2 00 C4&#xff08;正确&#xff09; EF AA 00 02 02 02 02 C2 00 C4 (错误格式) 一个字节用两个十六进制的数表示。错误示范的指令它的校验码错误的。校验码根据手册将字节相加计算。

GitStats - 统计Git所有提交记录工具

如果你是研发效能组的一员或者在从事 CI/CD 或 DevOps&#xff0c;除了提供基础设施&#xff0c;指标和数据是也是一个很重要的一环&#xff0c;比如需要分析下某个 Git 仓库代码提交情况&#xff1a; 该仓库的代码谁提交的代码最多 该仓库的活跃度是什么样子的 各个时段的提交…

c++ 纯虚函数、抽象类

一、 纯虚函数 抽象类 只要有一个纯虚函数&#xff0c;这个类称为抽象类 抽象类的特点 1、无法实例化 2、抽象类的子类&#xff0c;必须要重写父类中的纯虚函数&#xff0c;否者也属于抽象类 例子一 #include <iostream> #include <string.h> using namespa…

正确设置PyTorch训练时使用的GPU资源

背景&#xff1a; 最近在使用Hugging Face的transformers api来进行预训练大模型的微调&#xff0c;机器是8卡的GPU&#xff0c;当我调用trainer.train()之后&#xff0c;发现8个GPU都被使用了&#xff0c;因为这个机器上面还有其他人跑的模型&#xff0c;当我进行训练的时候&…

Python 爬虫实战之爬淘宝商品并做数据分析

前言 是这样的&#xff0c;之前接了一个金主的单子&#xff0c;他想在淘宝开个小鱼零食的网店&#xff0c;想对目前这个市场上的商品做一些分析&#xff0c;本来手动去做统计和分析也是可以的&#xff0c;这些信息都是对外展示的&#xff0c;只是手动比较麻烦&#xff0c;所以…