1.验证码服务
可以新建一个验证码模块,也可以直接在user模块
加入依赖
<dependency>
<groupId>com.github.penggle</groupId>
<artifactId>kaptcha</artifactId>
<version>2.3.2</version>
</dependency>图片验证码生成器配置
package com.dreams.creation.config;
import com.google.code.kaptcha.impl.DefaultKaptcha;
import com.google.code.kaptcha.util.Config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.Properties;
/**
* Kaptcha图片验证码配置类
**/
@Configuration
public class KaptchaConfig {
//图片验证码生成器,使用开源的kaptcha
@Bean
public DefaultKaptcha producer() {
Properties properties = new Properties();
properties.put("kaptcha.border", "no");
properties.put("kaptcha.textproducer.font.color", "black");
properties.put("kaptcha.textproducer.char.space", "10");
properties.put("kaptcha.textproducer.char.length","4");
properties.put("kaptcha.image.height","34");
properties.put("kaptcha.image.width","138");
properties.put("kaptcha.textproducer.font.size","25");
properties.put("kaptcha.noise.impl","com.google.code.kaptcha.impl.NoNoise");
Config config = new Config(properties);
DefaultKaptcha defaultKaptcha = new DefaultKaptcha();
defaultKaptcha.setConfig(config);
return defaultKaptcha;
}
}验证码的两个类
验证码生成结果类
package com.dreams.creation.model.dto.checkCode;
import lombok.Data;
/**
* 验证码生成结果类
*/
@Data
public class CheckCodeResultDto {
/**
* key用于验证
*/
private String key;
/**
* 混淆后的内容
* 举例:
* 1.图片验证码为:图片base64编码
* 2.短信验证码为:null
* 3.邮件验证码为: null
* 4.邮件链接点击验证为:null
* ...
*/
private String aliasing;
}验证码生成参数类
package com.dreams.creation.model.dto.checkCode;
import lombok.Data;
/**
* 验证码生成参数类
*/
@Data
public class CheckCodeParamsDto {
/**
* 验证码类型:pic、sms、email等
*/
private String checkCodeType;
/**
* 业务携带参数
*/
private String param1;
private String param2;
private String param3;
}
工具类
package com.dreams.creation.utils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.util.Base64;
public class EncryptUtil {
private static final Logger logger = LoggerFactory.getLogger(EncryptUtil.class);
public static String encodeBase64(byte[] bytes){
String encoded = Base64.getEncoder().encodeToString(bytes);
return encoded;
}
public static byte[] decodeBase64(String str){
byte[] bytes = null;
bytes = Base64.getDecoder().decode(str);
return bytes;
}
public static String encodeUTF8StringBase64(String str){
String encoded = null;
try {
encoded = Base64.getEncoder().encodeToString(str.getBytes("utf-8"));
} catch (UnsupportedEncodingException e) {
logger.warn("不支持的编码格式",e);
}
return encoded;
}
public static String decodeUTF8StringBase64(String str){
String decoded = null;
byte[] bytes = Base64.getDecoder().decode(str);
try {
decoded = new String(bytes,"utf-8");
}catch(UnsupportedEncodingException e){
logger.warn("不支持的编码格式",e);
}
return decoded;
}
public static String encodeURL(String url) {
String encoded = null;
try {
encoded = URLEncoder.encode(url, "utf-8");
} catch (UnsupportedEncodingException e) {
logger.warn("URLEncode失败", e);
}
return encoded;
}
public static String decodeURL(String url) {
String decoded = null;
try {
decoded = URLDecoder.decode(url, "utf-8");
} catch (UnsupportedEncodingException e) {
logger.warn("URLDecode失败", e);
}
return decoded;
}
public static void main(String [] args){
String str = "abcd{'a':'b'}";
String encoded = EncryptUtil.encodeUTF8StringBase64(str);
String decoded = EncryptUtil.decodeUTF8StringBase64(encoded);
System.out.println(str);
System.out.println(encoded);
System.out.println(decoded);
String url = "== wo";
String urlEncoded = EncryptUtil.encodeURL(url);
String urlDecoded = EncryptUtil.decodeURL(urlEncoded);
System.out.println(url);
System.out.println(urlEncoded);
System.out.println(urlDecoded);
}
}
验证码接口
package com.dreams.creation.service;
import com.dreams.creation.model.dto.checkCode.CheckCodeParamsDto;
import com.dreams.creation.model.dto.checkCode.CheckCodeResultDto;
/**
* 验证码接口
*/
public interface CheckCodeService {
/**
* @description 生成验证码
* @param checkCodeParamsDto 生成验证码参数
* @return CheckCodeResultDto 验证码结果
*/
CheckCodeResultDto generate(CheckCodeParamsDto checkCodeParamsDto);
/**
* @description 校验验证码
* @param key
* @param code
* @return boolean
*/
public boolean verify(String key, String code);
/**
* @description 验证码生成器
*/
public interface CheckCodeGenerator{
/**
* 验证码生成
* @return 验证码
*/
String generate(int length);
}
/**
* @description key生成器
*/
public interface KeyGenerator{
/**
* key生成
* @return 验证码
*/
String generate(String prefix);
}
/**
* @description 验证码存储
*/
public interface CheckCodeStore{
/**
* @description 向缓存设置key
* @param key key
* @param value value
* @param expire 过期时间,单位秒
* @return void
*/
void set(String key, String value, Integer expire);
String get(String key);
void remove(String key);
}
}抽象类AbstractCheckCodeService实现验证码接口
package com.dreams.creation.service;
import com.dreams.creation.model.dto.checkCode.CheckCodeParamsDto;
import com.dreams.creation.model.dto.checkCode.CheckCodeResultDto;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
/**
* 验证码接口
*/
@Slf4j
public abstract class AbstractCheckCodeService implements CheckCodeService {
protected CheckCodeGenerator checkCodeGenerator;
protected KeyGenerator keyGenerator;
protected CheckCodeStore checkCodeStore;
public abstract void setCheckCodeGenerator(CheckCodeGenerator checkCodeGenerator);
public abstract void setKeyGenerator(KeyGenerator keyGenerator);
public abstract void setCheckCodeStore(CheckCodeStore CheckCodeStore);
/**
* @description 生成验证公用方法
* @param checkCodeParamsDto 生成验证码参数
* @param code_length 验证码长度
* @param keyPrefix key的前缀
* @param expire 过期时间
* @return GenerateResult 生成结果
*/
public GenerateResult generate(CheckCodeParamsDto checkCodeParamsDto, Integer code_length, String keyPrefix, Integer expire){
//生成四位验证码
String code = checkCodeGenerator.generate(code_length);
log.debug("生成验证码:{}",code);
//生成一个key
String key = keyGenerator.generate(keyPrefix);
//存储验证码
checkCodeStore.set(key,code,expire);
//返回验证码生成结果
GenerateResult generateResult = new GenerateResult();
generateResult.setKey(key);
generateResult.setCode(code);
return generateResult;
}
@Data
protected class GenerateResult{
String key;
String code;
}
public abstract CheckCodeResultDto generate(CheckCodeParamsDto checkCodeParamsDto);
public boolean verify(String key, String code){
if (StringUtils.isBlank(key) || StringUtils.isBlank(code)){
return false;
}
String code_l = checkCodeStore.get(key);
if (code_l == null){
return false;
}
boolean result = code_l.equalsIgnoreCase(code);
if(result){
//删除验证码
checkCodeStore.remove(key);
}
return result;
}
}使用本地内存存储验证码
package com.dreams.creation.service.impl;
import com.dreams.creation.service.CheckCodeService;
import org.springframework.stereotype.Component;
import java.util.HashMap;
import java.util.Map;
/**
* 使用本地内存存储验证码
*/
@Component("MemoryCheckCodeStore")
public class MemoryCheckCodeStore implements CheckCodeService.CheckCodeStore {
Map<String,String> map = new HashMap<String,String>();
@Override
public void set(String key, String value, Integer expire) {
map.put(key,value);
}
@Override
public String get(String key) {
return map.get(key);
}
@Override
public void remove(String key) {
map.remove(key);
}
}使用redis存储验证码
package com.dreams.creation.service.impl;
import com.dreams.creation.service.CheckCodeService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import java.util.concurrent.TimeUnit;
/**
* 使用redis存储验证码
*/
@Component("RedisCheckCodeStore")
public class RedisCheckCodeStore implements CheckCodeService.CheckCodeStore {
@Autowired
RedisTemplate redisTemplate;
@Override
public void set(String key, String value, Integer expire) {
redisTemplate.opsForValue().set(key,value,expire, TimeUnit.SECONDS);
}
@Override
public String get(String key) {
return (String) redisTemplate.opsForValue().get(key);
}
@Override
public void remove(String key) {
redisTemplate.delete(key);
}
}数字字母生成器
package com.dreams.creation.service.impl;
import com.dreams.creation.service.CheckCodeService;
import org.springframework.stereotype.Component;
import java.util.Random;
/**
* 数字字母生成器
*/
@Component("NumberLetterCheckCodeGenerator")
public class NumberLetterCheckCodeGenerator implements CheckCodeService.CheckCodeGenerator {
@Override
public String generate(int length) {
String str="ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
Random random=new Random();
StringBuffer sb=new StringBuffer();
for(int i=0;i<length;i++){
int number=random.nextInt(36);
sb.append(str.charAt(number));
}
return sb.toString();
}
}uuid生成器
package com.dreams.creation.service.impl;
import com.dreams.creation.service.CheckCodeService;
import org.springframework.stereotype.Component;
import java.util.UUID;
/**
* uuid生成器
*/
@Component("UUIDKeyGenerator")
public class UUIDKeyGenerator implements CheckCodeService.KeyGenerator {
@Override
public String generate(String prefix) {
String uuid = UUID.randomUUID().toString();
return prefix + uuid.replaceAll("-", "");
}
}
接下来就是图片验证码生成服务
package com.dreams.creation.service.impl;
import com.dreams.creation.model.dto.checkCode.CheckCodeParamsDto;
import com.dreams.creation.model.dto.checkCode.CheckCodeResultDto;
import com.dreams.creation.service.AbstractCheckCodeService;
import com.dreams.creation.service.CheckCodeService;
import com.dreams.creation.utils.EncryptUtil;
import com.google.code.kaptcha.impl.DefaultKaptcha;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import sun.misc.BASE64Encoder;
import javax.annotation.Resource;
import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
/**
* 图片验证码生成器
*/
@Service("PicCheckCodeService")
public class PicCheckCodeServiceImpl extends AbstractCheckCodeService implements CheckCodeService {
@Autowired
private DefaultKaptcha kaptcha;
@Resource(name="NumberLetterCheckCodeGenerator")
@Override
public void setCheckCodeGenerator(CheckCodeGenerator checkCodeGenerator) {
this.checkCodeGenerator = checkCodeGenerator;
}
@Resource(name="UUIDKeyGenerator")
@Override
public void setKeyGenerator(KeyGenerator keyGenerator) {
this.keyGenerator = keyGenerator;
}
@Resource(name="RedisCheckCodeStore")
@Override
public void setCheckCodeStore(CheckCodeStore checkCodeStore) {
this.checkCodeStore = checkCodeStore;
}
@Override
public CheckCodeResultDto generate(CheckCodeParamsDto checkCodeParamsDto) {
GenerateResult generate = generate(checkCodeParamsDto, 4, "checkcode:", 300);
String key = generate.getKey();
String code = generate.getCode();
String pic = createPic(code);
CheckCodeResultDto checkCodeResultDto = new CheckCodeResultDto();
checkCodeResultDto.setAliasing(pic);
checkCodeResultDto.setKey(key);
return checkCodeResultDto;
}
private String createPic(String code) {
// 生成图片验证码
ByteArrayOutputStream outputStream = null;
BufferedImage image = kaptcha.createImage(code);
outputStream = new ByteArrayOutputStream();
String imgBase64Encoder = null;
try {
// 对字节数组Base64编码
BASE64Encoder base64Encoder = new BASE64Encoder();
ImageIO.write(image, "png", outputStream);
imgBase64Encoder = "data:image/png;base64," + EncryptUtil.encodeBase64(outputStream.toByteArray());
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
outputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
return imgBase64Encoder;
}
}
controller层调用它
package com.dreams.creation.controller;
import com.dreams.creation.model.dto.checkCode.CheckCodeParamsDto;
import com.dreams.creation.model.dto.checkCode.CheckCodeResultDto;
import com.dreams.creation.service.CheckCodeService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiImplicitParam;
import io.swagger.annotations.ApiImplicitParams;
import io.swagger.annotations.ApiOperation;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
/**
* 验证码服务接口
*/
@Api(value = "验证码服务接口")
@RestController
@RequestMapping("/checkCode")
public class CheckCodeController {
@Resource(name = "PicCheckCodeService")
private CheckCodeService picCheckCodeService;
@ApiOperation(value="生成验证信息", notes="生成验证信息")
@PostMapping(value = "/picCode")
public CheckCodeResultDto generatePicCheckCode(CheckCodeParamsDto checkCodeParamsDto){
return picCheckCodeService.generate(checkCodeParamsDto);
}
@ApiOperation(value="校验", notes="校验")
@ApiImplicitParams({
@ApiImplicitParam(name = "name", value = "业务名称", required = true, dataType = "String", paramType="query"),
@ApiImplicitParam(name = "key", value = "验证key", required = true, dataType = "String", paramType="query"),
@ApiImplicitParam(name = "code", value = "验证码", required = true, dataType = "String", paramType="query")
})
@PostMapping(value = "/verify/code")
public Boolean verify(String key, String code){
Boolean isSuccess = picCheckCodeService.verify(key,code);
return isSuccess;
}
}接下来定义Feign接口
package com.dreams.creation.service;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
@FeignClient(value = "dreams-checkCode-service",fallbackFactory = CheckCodeClientFactory.class)
@RequestMapping("/checkCode")
public interface CheckCodeClient {
@PostMapping(value = "/verify/code")
public Boolean verify(@RequestParam("key") String key, @RequestParam("code") String code);
}以及降级逻辑
package com.dreams.creation.service;
import feign.hystrix.FallbackFactory;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
@Slf4j
@Component
public class CheckCodeClientFactory implements FallbackFactory<CheckCodeClient> {
@Override
public CheckCodeClient create(Throwable throwable) {
return new CheckCodeClient() {
@Override
public Boolean verify(String key, String code) {
log.debug("调用验证码服务熔断异常:{}", throwable.getMessage());
return null;
}
};
}
}熔断降级需要在启动类加上注解
@EnableFeignClients(basePackages = {"com.dreams.creation.service"})
请求
POST http://localhost:8101/api/user/checkCode/picCode


他会存储到redis里面

请求确认
POST http://localhost:8102/api/user/checkCode/verify/code?key=checkcode:b8b2d66638d4434ca87ba53f8cbd0806&code=7TTT
可以看到返回true

redis里的key就会被删除
2.账号名密码方式登录
注入验证码service,实现AuthService,来实现账号名密码方式登录的逻辑
package com.dreams.creation.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.dreams.creation.model.dto.user.AuthParamsDto;
import com.dreams.creation.model.dto.user.DcUserExt;
import com.dreams.creation.model.entity.DcUser;
import com.dreams.creation.service.AuthService;
import com.dreams.creation.service.CheckCodeClient;
import com.dreams.creation.service.UserFeignClient;
import org.apache.commons.lang.StringUtils;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
/**
* 账号名密码方式
*/
@Service("password_authService")
public class PasswordAuthServiceImpl implements AuthService {
@Autowired
UserFeignClient userFeignClient;
@Autowired
PasswordEncoder passwordEncoder;
@Autowired
CheckCodeClient checkCodeClient;
@Override
public DcUserExt execute(AuthParamsDto authParamsDto) {
//账号
String userAccount = authParamsDto.getUserAccount();
//输入的验证码
String checkcode = authParamsDto.getCheckCode();
//验证码对应的key
String checkcodekey = authParamsDto.getCheckCodeKey();
if (StringUtils.isEmpty(checkcode) || StringUtils.isEmpty(checkcodekey)) {
throw new RuntimeException("请输入的验证码");
}
//远程调用验证码服务接口去校验验证码
Boolean verify = checkCodeClient.verify(checkcodekey, checkcode);
if (verify == null || !verify) {
throw new RuntimeException("验证码输入错误");
}
//账号是否存在
//根据username账号查询数据库
DcUser dcUser = userFeignClient.selectOne(new LambdaQueryWrapper<DcUser>().eq(DcUser::getUserAccount, userAccount));
//查询到用户不存在,要返回null即可,spring security框架抛出异常用户不存在
if (dcUser == null) {
throw new RuntimeException("账号不存在");
}
//验证密码是否正确
//如果查到了用户拿到正确的密码
String passwordDb = dcUser.getUserPassword();
//拿 到用户输入的密码
String passwordForm = authParamsDto.getUserPassword();
//校验密码
boolean matches = passwordEncoder.matches(passwordForm, passwordDb);
if (!matches) {
throw new RuntimeException("账号或密码错误");
}
DcUserExt dcUserExt = new DcUserExt();
BeanUtils.copyProperties(dcUser, dcUserExt);
return dcUserExt;
}
}这样UserDetailsServiceImpl里面就可以直接调用了,后面加入微信扫码认证也不需要修改UserDetailsServiceImpl。
package com.dreams.creation.service.impl;
import com.alibaba.fastjson.JSON;
import com.dreams.creation.model.dto.user.AuthParamsDto;
import com.dreams.creation.model.dto.user.DcUserExt;
import com.dreams.creation.service.AuthService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
/**
* @author PoemsAndDreams
* @date 2025-01-16 18:57
* @description //TODO
*/
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
ApplicationContext applicationContext;
@Override
public UserDetails loadUserByUsername(String authParams) throws UsernameNotFoundException {
AuthParamsDto authParamsDto = null;
try {
//将认证参数转为AuthParamsDto类型
authParamsDto = JSON.parseObject(authParams, AuthParamsDto.class);
} catch (Exception e) {
// todo 日志
throw new RuntimeException("认证请求数据格式不对");
}
//认证方法
String authType = authParamsDto.getAuthType();
AuthService authService = applicationContext.getBean(authType + "_authService",AuthService.class);
DcUserExt user = authService.execute(authParamsDto);
return getUserPrincipal(user);
}
private UserDetails getUserPrincipal(DcUserExt user) {
//取出数据库存储的正确密码
String password =user.getUserPassword();
//用户权限,如果不加报Cannot pass a null GrantedAuthority collection
String[] authorities= {"test"};
//为了安全在令牌中不放密码
user.setUserPassword(null);
//将user对象转json
String userString = JSON.toJSONString(user);
//创建UserDetails对象
UserDetails userDetails = User.withUsername(userString).password(password).authorities(authorities).build();
return userDetails;
}
}
3.用户授权
RBAC分为两种方式:
- 基于角色的访问控制(Role-Based Access Control)
- 基于资源的访问控制(Resource-Based Access Control)
角色的访问控制(Role-Based Access Control)是按角色进行授权
基于资源的访问控制(Resource-Based Access Control)是按资源(或权限)进行授权
基于资源的访问控制系统可扩展性强,所以建议使用基于资源的访问控制
用到的表如下:
--
-- Table structure for table `menu`
--
DROP TABLE IF EXISTS `menu`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `menu`
(
`id` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
`code` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '菜单编码',
`p_id` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '父菜单ID',
`menu_name` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '名称',
`url` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '请求地址',
`is_menu` char(1) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '是否是菜单',
`level` int DEFAULT NULL COMMENT '菜单层级',
`sort` int DEFAULT NULL COMMENT '菜单排序',
`status` char(1) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL,
`icon` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL,
`create_time` datetime DEFAULT NULL,
`update_time` datetime DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE,
UNIQUE KEY `FK_CODE` (`code`) USING BTREE
) ENGINE = InnoDB
DEFAULT CHARSET = utf8mb3
ROW_FORMAT = DYNAMIC;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Table structure for table `permission`
--
DROP TABLE IF EXISTS `permission`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `permission`
(
`id` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
`role_id` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
`menu_id` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
`create_time` datetime DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE,
UNIQUE KEY `permission_unique` (`role_id`, `menu_id`) USING BTREE,
KEY `permission_menu_id` (`menu_id`) USING BTREE
) ENGINE = InnoDB
DEFAULT CHARSET = utf8mb3
ROW_FORMAT = DYNAMIC;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Table structure for table `role`
--
DROP TABLE IF EXISTS `role`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `role`
(
`id` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
`role_name` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL,
`role_code` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL,
`description` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL,
`create_time` datetime DEFAULT NULL,
`update_time` datetime DEFAULT NULL,
`status` char(1) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
PRIMARY KEY (`id`) USING BTREE,
UNIQUE KEY `unique_role_name` (`role_name`) USING BTREE,
UNIQUE KEY `unique_role_value` (`role_code`) USING BTREE
) ENGINE = InnoDB
DEFAULT CHARSET = utf8mb3
ROW_FORMAT = DYNAMIC;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Table structure for table `dcUser`
--
CREATE TABLE `dcUser`
(
`id` bigint NOT NULL AUTO_INCREMENT COMMENT 'id',
`userAccount` varchar(256) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '账号',
`userPassword` varchar(512) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '密码',
`userName` varchar(256) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '用户昵称',
`userAvatar` varchar(1024) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '用户头像',
`userProfile` varchar(512) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '用户简介',
`sex` char(1) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL,
`email` varchar(45) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL,
`cellphone` varchar(45) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL,
`qq` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL,
`editTime` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '编辑时间',
`createTime` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updateTime` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`isDelete` tinyint NOT NULL DEFAULT '0' COMMENT '是否删除',
PRIMARY KEY (`id`)
) ENGINE = InnoDB
AUTO_INCREMENT = 7
DEFAULT CHARSET = utf8mb4
COLLATE = utf8mb4_unicode_ci COMMENT ='用户';
--
-- Table structure for table `user_role`
--
DROP TABLE IF EXISTS `user_role`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `user_role`
(
`id` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
`user_id` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL,
`role_id` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL,
`create_time` datetime DEFAULT NULL,
`creator` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE,
KEY `user_role_user_id` (`user_id`) USING BTREE,
KEY `user_role_role_id` (`role_id`) USING BTREE
) ENGINE = InnoDB
DEFAULT CHARSET = utf8mb3
ROW_FORMAT = DYNAMIC;
/*!40101 SET character_set_client = @saved_cs_client */;在用户模块完成上面表的增删改查后
在认证模块的UserDetailsServiceImpl文件
搜索出来上面表的code字段列表,赋值进去就行了
mapper方法
@Select("SELECT * FROM menu WHERE id IN (SELECT menuId FROM role_menu WHERE roleId IN ( SELECT roleId FROM user_role WHERE userId = #{userId} ))")
List<Menu> selectPermissionByUserId(@Param("userId") String userId);getUserPrincipal方法
private UserDetails getUserPrincipal(DcUserExt user) {
//取出数据库存储的正确密码
String password =user.getUserPassword();
//用户权限,如果不加报Cannot pass a null GrantedAuthority collection
String[] authorities= {"test"};
//搜索 数据库 拿到 该用户对应角色拥有的权限菜单
//......
//根据用户id查询用户的权限
List<Menu> menus = userFeignClient.selectPermissionByUserId(user.getId().toString());
if(!Objects.isNull(menus)){
List<String> permissions =new ArrayList<>();
menus.forEach(m->{
//拿到了用户拥有的权限标识符
permissions.add(m.getCode());
});
//将permissions转成数组
authorities = permissions.toArray(new String[0]);
}
//为了安全在令牌中不放密码
user.setUserPassword(null);
//将user对象转json
String userString = JSON.toJSONString(user);
//创建UserDetails对象
UserDetails userDetails = User.withUsername(userString).password(password).authorities(authorities).build();
return userDetails;
}再在相对应的controller方法加入注解
@PreAuthorize("hasAuthority('code字段值')")那么只有拥有这个权限的用户才能调用该请求
4.异常捕获
全局异常捕获
在SpringSecurity出现异常会被ExceptionTranslationFilter捕获到。
认证过程异常会被封装成AuthenticationException然后调用AuthenticationEntryPoint对象的方法去进行异常处理。
授权过程异常会被封装成AccessDeniedException然后调用AccessDeniedHandler对象的方法去进行异常处理。
因为公共模块没有引入security依赖,所以无法定义处理这两个异常,所以我们可以给每一个资源服务定义一个全局异常处理
不过注意Bean名字为资源名+GlobalExceptionHandler
否则会重名Bean
认证流程在网关处理,所以资源服务不会出现认证问题,可以不用定义认证异常
package com.dreams.creation.config;
import com.dreams.creation.common.BaseResponse;
import com.dreams.creation.common.ErrorCode;
import com.dreams.creation.common.ResultUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
/**
* 全局异常处理器
*/
@RestControllerAdvice
@Slf4j
public class UserGlobalExceptionHandler {
@ExceptionHandler(AccessDeniedException.class)
public BaseResponse<?> accessDeniedExceptionHandler(AccessDeniedException e) {
log.error("AccessDeniedException", e);
return ResultUtils.error(ErrorCode.NO_AUTH_ERROR, "警告,无权限!");
}
}
5.前端请求
这里使用React举例
import Footer from '@/components/Footer';
import {getLoginUserUsingGet, userLoginUsingPost} from '@/services/user/userController';
import {ClockCircleOutlined, LockOutlined, UserOutlined} from '@ant-design/icons';
import {LoginForm, ProFormText} from '@ant-design/pro-components';
import {useEmotionCss} from '@ant-design/use-emotion-css';
import {Helmet, history, useModel} from '@umijs/max';
import {message, Tabs} from 'antd';
import React, {useEffect, useState} from 'react';
import {Link} from 'umi';
import Settings from '../../../../config/defaultSettings';
import axios from "axios";
type AuthParamsDto = {
userAccount?: string;
userPassword?: string;
cellphone?: string;
checkCode?: string;
checkCodeKey?: string;
authType?: string;
};
const Login: React.FC = () => {
const [type, setType] = useState<string>('account');
const {initialState, setInitialState} = useModel('@@initialState');
const containerClassName = useEmotionCss(() => {
return {
display: 'flex',
flexDirection: 'column',
height: '100vh',
overflow: 'auto',
backgroundImage:
"url('https://mdn.alipayobjects.com/yuyan_qk0oxh/afts/img/V-_oS6r-i7wAAAAAAAAAAAAAFl94AQBr')",
backgroundSize: '100% 100%',
};
});
const handleSubmit = async (values: AuthParamsDto) => {
try {
// 格式化请求数据
const requestData = {
client_id: 'DreamsWebApp',
client_secret: 'DreamsWebApp',
grant_type: 'password',
username: JSON.stringify({
userAccount: values.userAccount,
authType: 'password', // 默认使用密码验证
userPassword: values.userPassword,
checkCode: values.checkCode,
checkCodeKey: captchaKey,
})
};
console.log("captchaKey", captchaKey)
// 发送 POST 请求
const res = await axios.post('http://localhost:8101/api/auth/oauth/token', null, {
params: requestData,
});
console.log("res", res)
localStorage.setItem('token', res.data.access_token); // 假设 token 存储在 localStorage 中
// // 登录
// const res = await userLoginUsingPost({
// ...values,
// });
const cur = await getLoginUserUsingGet();
const defaultLoginSuccessMessage = '登录成功!';
message.success(defaultLoginSuccessMessage);
// 保存已登录用户信息
setInitialState({
...initialState,
currentUser: cur.data,
});
const urlParams = new URL(window.location.href).searchParams;
history.push(urlParams.get('redirect') || '/');
return;
} catch (error: any) {
const defaultLoginFailureMessage = `登录失败 , ${error.response.data.error_description} , 请重试!`;
message.error(defaultLoginFailureMessage);
// 自动刷新验证码
fetchCaptcha();
}
};
const [captchaKey, setCaptchaKey] = useState<string>(''); // 用于保存验证码的 key
const [captchaImage, setCaptchaImage] = useState<string>(''); // 用于保存验证码的 base64 图像
const [isLoading, setIsLoading] = useState<boolean>(false); // 控制验证码加载状态
// 获取验证码
const fetchCaptcha = async () => {
try {
setIsLoading(true);
// const response = await generatePicCheckCodeUsingPost({}) // 需要调整为实际获取验证码的 API
//
const response = await axios.post('http://localhost:8101/api/user/checkCode/picCode'); // 需要调整为实际获取验证码的 API
const {key, aliasing} = response.data;
setCaptchaKey(key);
setCaptchaImage(aliasing);
} catch (error) {
message.error('获取验证码失败,请稍后再试!');
} finally {
setIsLoading(false);
}
};
// 在组件挂载时请求验证码
useEffect(() => {
fetchCaptcha();
}, []);
return (
<div className={containerClassName}>
<Helmet>
<title>
{'登录'}- {Settings.title}
</title>
</Helmet>
<div
style={{
flex: '1',
padding: '32px 0',
}}
>
<LoginForm
contentStyle={{
minWidth: 280,
maxWidth: '75vw',
}}
logo={<img alt="logo" style={{height: '100%'}} src="/logo.svg"/>}
title="Dreams智能创作中心"
subTitle={'Dreams智能创作中心'}
initialValues={{
autoLogin: true,
}}
onFinish={async (values) => {
await handleSubmit(values as AuthParamsDto);
}}
>
<Tabs
activeKey={type}
onChange={setType}
centered
items={[
{
key: 'account',
label: '账户密码登录',
},
]}
/>
{type === 'account' && (
<>
<ProFormText
name="userAccount"
fieldProps={{
size: 'large',
prefix: <UserOutlined/>,
}}
placeholder={'请输入账号'}
rules={[
{
required: true,
message: '账号是必填项!',
},
]}
/>
<ProFormText.Password
name="userPassword"
fieldProps={{
size: 'large',
prefix: <LockOutlined/>,
}}
placeholder={'请输入密码'}
rules={[
{
required: true,
message: '密码是必填项!',
},
]}
/>
{/* 显示验证码图片和输入框在同一行 */}
<div style={{display: 'flex', alignItems: 'center'}}>
<img src={captchaImage} alt="验证码"
onClick={fetchCaptcha} // 点击图片时重新获取验证码
style={{width: 100, height: 40, marginRight: 20, marginBottom: 20}}/>
<ProFormText
name="checkCode"
fieldProps={{
size: 'large',
prefix: <ClockCircleOutlined/>,
}}
placeholder={'请输入验证码'}
rules={[
{
required: true,
message: '验证码是必填项!',
},
]}
/>
</div>
</>
)}
<div
style={{
marginBottom: 24,
textAlign: 'right',
}}
>
<Link to="/user/register">用户注册</Link>
</div>
</LoginForm>
</div>
<Footer/>
</div>
);
};
export default Login;
主要看下面请求
请求验证码

这里是请求登录

localStorage 是浏览器提供的一种机制,用于在浏览器中存储数据。这些数据会被保留直到显式地删除,即使浏览器关闭后,数据也不会丢失。
localStorage.setItem('token', res.data.access_token); // 假设 token 存储在 localStorage 中然后请求获取当前登录用户
存储到全局

access.ts会用到这个状态

如果登录失败,需要刷新验证码

打开requestConfig.ts或requestErrorConfig.ts修改请求拦截器,让所有请求加上token
// 请求拦截器
requestInterceptors: [
(config: RequestOptions) => {
// 获取存储在 localStorage 中的 token(假设 token 存储在 localStorage)
const token = localStorage.getItem('token');
// 如果 token 存在,将 token 加入到请求头中
if (token) {
config.headers['Authorization'] = `Bearer ${token}`;
}
// 拦截请求配置,进行个性化处理。
return config;
},
],如果是Upload组件
const token = localStorage.getItem('token'); // 获取 token<Upload
action={`${BACKEND_HOST_LOCAL}api/article/file/upload`}
listType="picture-card"
data={{
biz: 'user_avatar', // 这里可以传递你所需的业务标识,例如“avatar”表示头像上传
}}
headers={{
'Authorization': `Bearer ${token}`, // 将 token 添加到 Authorization 头
}}
beforeUpload={handleBeforeUpload} // 上传前处理
fileList={fileList} // 文件列表状态
maxCount={1} // 限制只能上传一个文件
withCredentials={true} // 确保跨域请求时携带 cookies
onChange={handleChange} // 文件变化时更新状态
>使用headers传入即可
那么还有退出登录也要改一下
从请求后端到localStorage.removeItem(‘token’)
const loginOut = async () => {
// 请求后端删除session的登录用户
//await userLogoutUsingPost();
//将从浏览器的 localStorage 中删除名为 'token' 的项。
localStorage.removeItem('token');
const { search, pathname } = window.location;
const urlParams = new URL(window.location.href).searchParams;
/** 此方法会跳转到 redirect 参数所在的位置 */
const redirect = urlParams.get('redirect');
// Note: There may be security issues, please note
if (window.location.pathname !== '/user/login' && !redirect) {
history.replace({
pathname: '/user/login',
search: stringify({
redirect: pathname + search,
}),
});
}
};
同样如果头像那里需要登录用户信息
在src/components/RightContent/AvatarDropdown.tsx请求拿到登录用户
const [loading, setLoading] = useState(false);
const [currentUser, setcurrentUser] = useState(); // 初始值设为空数组
const loadData = async () => {
setLoading(true);
try {
const res = await getLoginUserUsingGet();
setcurrentUser(res.data)
} catch (error: any) {
message.error('请求失败' + error.message);
} finally {
setLoading(false);
}
};
useEffect(() => {
loadData();
}, []);该文件下就可以正常展示了

5.参考内容
黑马-学成在线
尚硅谷-尚上优选


