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

合家云社区物业管理平台

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

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
课程简介:历经半个多月的时间,Debug亲自撸的 “企业员工角色权限管理平台” 终于完成了。正如字面意思,本课程讲解的是一个真正意义上的、企业级的项目实战,主要介绍了企业级应用系统中后端应用权限的管理,其中主要涵盖了六大核心业务模块、十几张数据库表。 其中的核心业务模块主要包括用户模块、部门模块、岗位模块、角色模块、菜单模块和系统日志模块;与此同时,Debug还亲自撸了额外的附属模块,包括字典管理模块、商品分类模块以及考勤管理模块等等,主要是为了更好地巩固相应的技术栈以及企业应用系统业务模块开发流程! 核心技术栈列表: 值得介绍的是,本课程在技术栈层面涵盖了前端和后端的大部分常用技术,包括Spring BootSpring MVC、Mybatis、Mybatis-Plus、Shiro(身份认证与资源授权跟会话等等)、Spring AOP、防止XSS攻击、防止SQL注入攻击、过滤器Filter、验证码Kaptcha、热部署插件Devtools、POI、Vue、LayUI、ElementUI、JQuery、HTML、Bootstrap、Freemarker、一键打包部署运行工具Wagon等等,如下图所示: 课程内容与收益: 总的来说,本课程是一门具有很强实践性质的“项目实战”课程,即“企业应用员工角色权限管理平台”,主要介绍了当前企业级应用系统中员工、部门、岗位、角色、权限、菜单以及其他实体模块的管理;其中,还重点讲解了如何基于Shiro的资源授权实现员工-角色-操作权限、员工-角色-数据权限的管理;在课程的最后,还介绍了如何实现一键打包上传部署运行项目等等。如下图所示为本权限管理平台的数据库设计图: 以下为项目整体的运行效果截图: 值得一提的是,在本课程中,Debug也向各位小伙伴介绍了如何在企业级应用系统业务模块开发中,前端到后端再到数据库,最后再到服务器的上线部署运行等流程,如下图所示:
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值