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

/**
* 通用常量类
* @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 = "操作失败";
}
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("-", "");
}
}
链式映射是指在 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 值类型
* @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;
}
}
添加序列化工具类,让Redis使用FastJson序列化,提高序列化效率, 将存储在Redis中的value值,序列化为JSON格式便于查看
public class FastJsonJsonRedisSerializer<T> implements RedisSerializer<T>
@Component
public class RedisCache{}
@Configuration
public class RedisConfig {}
<dependency>
<groupId>com.github.whvcsegroupId>
<artifactId>easy-captchaartifactId>
<version>1.6.2version>
dependency>
# 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
@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());
}
}
com.msb.hjycommunity.system.domain.SysUserpublic 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;
}
//......
}
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);
}
}
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);
}
}

/**
* 用户验证处理
* @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;
}
}
/**
* 登录用户 身份权限对象
* @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;
}
}
SecurityConfigcom.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 注解支持,该注解不支持权限表达式
@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(),"认证失败,无法访问系统资源")));
}
}
//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;
}
}
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);
}
}
主配置文件中添加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 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;
}
}
/**
* 登录验证
* @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);
}
}
/**
* 业务异常
* @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;
}
}
/**
* 验证码异常
* @author spikeCong
* @date 2023/5/4
**/
public class CaptchaNotMatchException extends CustomException {
public CaptchaNotMatchException() {
super("验证码错误",400);
}
}
/**
* 响应结果封装对象
* @author spikeCong
* @date 2023/2/28
**/
public class BaseResponse 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
*/
public static BaseResponse fail(String code,String message,boolean success){
BaseResponse response = new BaseResponse<>();
response.setCode(code);
response.setMsg(message);
response.setSuccess(success);
return response;
}
}
/**
* 全局异常处理器
* @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());
}
}
获取验证码: http://localhost:9999/hejiayun/captchaImage

查看redis中的验证码

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

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

// 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 permissions;
//一定要有空参构造,否则序列化会失败
public LoginUser() {
}
public LoginUser(SysUser user, Set permissions) {
this.user = user;
this.permissions = permissions;
}
}
在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 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;
}
}
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;
}
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();
}
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);
}
}
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);
}
}
当用户再次发送请求的时候,要进行校验,用户会携带登录时生成的JWT,所以我们需要自定义一个Jwt认证过滤器

自定义一个过滤器,这个过滤器会去获取请求头中的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);
}
}
/**
* 通用配置
* @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);
}
}
/**
* 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();
}
}






/**
* 角色表 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;
}
}
/**
* 菜单权限表 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 children = new ArrayList();
}
/**
* 角色表 数据层
* @author spikeCong
* @date 2023/5/9
**/
public interface SysRoleMapper extends BaseMapper {
/**
* 根据用户ID 查询角色
* @param userId
* @return: 角色列表
*/
public List selectRolePermissionByUserId(Long userId);
}
/**
* 角色业务层
* @author spikeCong
* @date 2023/5/9
**/
public interface SysRoleService {
/**
* 根据用户ID查询角色信息
* @param userId
* @return: 角色权限列表
*/
public Set 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 selectRolePermissionByUserId(Long userId) {
//根据用户Id获取角色信息
List roleList = sysRoleMapper.selectRolePermissionByUserId(userId);
//将角色信息List集合转换为Set集合
Set permsSet = new HashSet<>();
for (String roleKey : roleList) {
if(!StringUtils.isEmpty(roleKey)){
permsSet.add(roleKey);
}
}
return permsSet;
}
}
/**
* 菜单表 数据层
* @author spikeCong
* @date 2023/5/9
**/
public interface SysMenuMapper extends BaseMapper {
/**
* 根据用户ID查询权限
*
* @param userId 用户ID
* @return 权限列表
*/
public List selectMenuPermsByUserId(Long userId);
}
/**
* 菜单业务层
* @author spikeCong
* @date 2023/5/9
**/
public interface SysMenuService {
/**
* 根据用户Id查询用户权限
* @param userId
* @return: java.util.Set
*/
public Set selectMenuPermsByUserId(Long userId);
}
/**
* @author spikeCong
* @date 2023/5/9
**/
@Service
public class SysMenuServiceImpl implements SysMenuService {
@Autowired
private SysMenuMapper menuMapper;
@Override
public Set selectMenuPermsByUserId(Long userId) {
List menuList = menuMapper.selectMenuPermsByUserId(userId);
Set permsSet = new HashSet<>();
for (String menu : menuList) {
if(!StringUtils.isEmpty(menu)){
permsSet.add(menu);
}
}
return permsSet;
}
}
通过查看接口文档可以发现,返回的用户信息中,要求包含 :
/**
* 用户表 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 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;
}
}
UserDetailsServiceImpl 中,调用的SysUserMapper中的 selectUserByUserName方法, 所以需要获取更加详细的用户信息的话,修改XML即可
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
@Test
public void testSelectUserByUserName(){
SysUser admin = userMapper.selectUserByUserName("admin");
System.out.println(admin);
}
//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 getRolePermission(SysUser user){
Set roles = new HashSet<>();
//管理员拥有所有权限
if(user.isAdmin()){
roles.add("admin");
}else{
roles = roleService.selectRolePermissionByUserId(user.getUserId());
}
return roles;
}
/**
* 获取菜单数据权限
* @param user
* @return: java.util.Set
*/
public Set getMenuPermission(SysUser user){
Set perms = new HashSet<>();
//管理员拥有所有权限
if(user.isAdmin()){
perms.add("*:*:*");
}else{
perms = menuService.selectMenuPermsByUserId(user.getUserId());
}
return perms;
}
}
/**
* 登录验证
* @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 roles = permissionService.getRolePermission(user);
//权限集合
Set 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;
}
}
在首页加载时,前端会向后端发送请求,获取左侧导航菜单及其子菜单数据.

/**
* 菜单表 数据层
* @author spikeCong
* @date 2023/5/9
**/
public interface SysMenuMapper extends BaseMapper {
/**
* 用户为admin时,查询全部菜单信息
* @param
* @return: 菜单列表
*/
public List selectMenuTreeAll();
}
/**
* 根据用户id 查询菜单信息
* @param
* @return: 菜单列表
*/
public List selectMenuTreeByUserId(Long userId);
/**
* 根据用户ID 查询菜单树信息
* @param userId
* @return: 菜单列表
*/
public List selectMenuTreeByUserId(Long userId);
@Override
public List selectMenuTreeByUserId(Long userId) {
List menus = null;
if(userId != null && 1L == userId){
menus = menuMapper.selectMenuTreeAll();
}else{
menus = menuMapper.selectMenuTreeByUserId(userId);
}
//todo 获取子菜单
return getChildPerms(menus,0);
}
/**
* 根据父节点ID 获取所有子节点
* @param menus
* @param parentId 传入的父节点Id
* @return: java.util.List
*/
private List getChildPerms(List menus, int parentId) {
List 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 menus, SysMenu m) {
//得到子节点列表,保存到父菜单的children中
List 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 getChildList(List menus, SysMenu m) {
List subMenus = menus.stream()
.filter(sub -> sub.getParentId().longValue() == m.getMenuId().longValue())
.collect(Collectors.toList());
return subMenus;
}
/**
* 获取路由信息
* @param
* @return: 路由信息
*/
@GetMapping("/getRouters")
public BaseResponse getRouters(){
LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
SysUser user = loginUser.getUser();
List menus = sysMenuService.selectMenuTreeByUserId(user.getUserId());
return BaseResponse.success(menus);
}

测试获取的JSON数据不符合接口文档要求
/**
* 路由配置信息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 children;
//get... set...
}
/**
* 路由显示信息
* @author spikeCong
* @date 2023/5/11
**/
public class MetaVo {
/**
* 设置该路由在侧边栏和面包屑中展示的名字
*/
private String title;
/**
* 设置该路由的图标,对应路径src/assets/icons/svg
*/
private String icon;
/**
* 设置为true,则不会被 缓存
*/
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";
}



/**
* 构建前端路由所需要的菜单
* @param menus 菜单列表
* @return: 路由列表
*/
public List buildMenus(List menus);
@Override
public List buildMenus(List menus) {
List 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;
}
@Override
public List buildMenus(List menus) {
List 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;
}
@Override
public List buildMenus(List menus) {
List 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;
}
@Override
public List buildMenus(List menus) {
List 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 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 menus = sysMenuService.selectMenuTreeByUserId(user.getUserId());
//转换为前端需要的路由列表
List routerVoList = sysMenuService.buildMenus(menus);
return BaseResponse.success(routerVoList);
}

添加permissions属性和构造方法
public class LoginUser implements UserDetails {
/**
* 用户信息
*/
private SysUser user;
/**
* 权限列表
*/
private Set permissions;
public LoginUser(SysUser user, Set permissions) {
this.user = user;
this.permissions = permissions;
}
}
/**
* 用户验证处理
* @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));
}
}
主要有以下几种校验方式
/**
* 自定义权限校验
* @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 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 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;
}
}
第一步: 添加权限校验
@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只有查看小区信息的权限,没有查看部门信息的权限

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

