合家云社区物业管理平台
4.权限管理模块研发
4.3 登录模块开发
前台和后台的认证授权统一都使用SpringSecurity安全框架来实现。首次登录过程如下图:
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
- 认证流程图
(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
查看redis中的验证码
访问登录接口,携带 用户名,密码,验证码,UUID: http://localhost:9999/hejiayun/login
转换一下token,查看载荷信息
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认证过滤器
- 获取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) 测试
- 获取验证码
- 发送登录请求,携带验证码
- 未携带token,查询小区数据
- 请求头中携带token访问
4.3.3 获取用户权限信息接口
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 获取路由导航菜单信息
在首页加载时,前端会向后端发送请求,获取左侧导航菜单及其子菜单数据.
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) 构建前端路由所需要的菜单
- 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);
}
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
账号登录
第三步: 获取用户 laoli
拥有的权限信息, laoli
只有查看小区信息的权限,没有查看部门信息的权限
第四步: 分别访问查询小区信息接口、查询部门信息接口
- 查询小区信息,可以获取数据
- 查询部门信息,被拦截 没有通过验证