1.前端请求验证码
| getCodeImg().then(response => { |
| this.codeUrl = "data:image/gif;base64," + response.captcha; |
| this.formLogin.uuid = response.uuid; |
| }); |
| export function getCodeImg (data) { |
| return request({ |
| url: '/api/auth/captcha', |
| method: 'get' |
| }) |
| } |
2.后端生成验证码
| @RequestMapping("/captcha") |
| public ObjectRestResponse captcha() throws Exception { |
| |
| SpecCaptcha specCaptcha = new SpecCaptcha(130, 48, 4); |
| |
| specCaptcha.setFont(new Font("Verdana", Font.PLAIN, 32)); |
| |
| specCaptcha.setCharType(Captcha.TYPE_ONLY_NUMBER); |
| |
| String uuid = UUIDUtils.generateShortUuid(); |
| String text = specCaptcha.text().toLowerCase(); |
| |
| stringRedisTemplate.opsForValue().set(String.format(REDIS_KEY_CAPTCHA, uuid), text, LOGIN_CAPTCHA_EXPIRATION, TimeUnit.MINUTES); |
| |
| ByteArrayOutputStream stream = new ByteArrayOutputStream(); |
| |
| specCaptcha.out(stream); |
| String captcha = Base64.byteArrayToBase64(stream.toByteArray()); |
| Map map = new HashMap<>(); |
| map.put("captcha", captcha); |
| map.put("uuid", uuid); |
| return new ObjectRestResponse().data(map); |
| } |
3.前端携带参数请求登录
| AccountLogin({ |
| username, |
| password, |
| uuid, |
| verCode |
| }) |
| export function AccountLogin (data) { |
| return request({ |
| url: '/api/auth/jwt/token', |
| method: 'post', |
| data, |
| loading: { |
| type: 'loading', |
| options: { |
| fullscreen: true, |
| lock: true, |
| text: '正在登陆...', |
| spinner: 'el-icon-loading', |
| background: 'rgba(0, 0, 0, 0.8)' |
| } |
| } |
| }) |
| } |
4.后端接受请求进行处理
4.1校验验证码
| |
| String sessionCode = stringRedisTemplate.opsForValue().get(String.format(REDIS_KEY_CAPTCHA, authenticationRequest.getUuid())); |
| if(sessionCode == null){ |
| throw new UserInvalidException("验证码已过期"); |
| } |
| |
| if (authenticationRequest.getVerCode() == null || !sessionCode.equals(authenticationRequest.getVerCode().trim().toLowerCase())) { |
| throw new UserInvalidException("验证码不正确"); |
| } |
4.2校验用户/密码
| UserInfo info = permissionService.validate(authenticationRequest.getUsername(), authenticationRequest.getPassword()); |
| public UserInfo validate(String username, String password) { |
| UserInfo info = new UserInfo(); |
| User user = userBiz.getUserByUsername(username); |
| if (encoder.matches(password, user.getPassword())) { |
| BeanUtils.copyProperties(user, info); |
| info.setId(user.getId().toString()); |
| } |
| return info; |
| } |
getUserByUsername 获取用户信息
| @Service |
| @Transactional(rollbackFor = Exception.class) |
| public class UserBiz extends BaseBiz<UserMapper,User> { |
| |
| |
| |
| |
| |
| public User getUserByUsername(String username){ |
| User user = new User(); |
| user.setUsername(username); |
| return mapper.selectOne(user); |
| } |
| } |
BaseBiz 类
BaseZiz类注入 Mapper对象,并通过 Mapper对象调用方法,进而可以在方法调用前后添加一些额外的操作
| public abstract class BaseBiz<M extends Mapper<T>, T> { |
| @Autowired |
| protected M mapper; |
| public void setMapper(M mapper) { |
| this.mapper = mapper; |
| } |
| |
| public T selectOne(T entity) { |
| return mapper.selectOne(entity); |
| } |
4.3校验通过生成 token
创建 JWTInfo 类
| JWTInfo jwtInfo = new JWTInfo(info.getUsername(), info.getId() + "", info.getName()); |
| public JWTInfo(String username, String userId, String name) { |
| this.username = username; |
| this.userId = userId; |
| this.name = name; |
| this.tokenId = UUIDUtils.generateShortUuid(); |
| } |
通过 JWT工具类生成 token
| String token = jwtTokenUtil.generateToken(jwtInfo); |
| |
| |
| |
| |
| |
| |
| |
| |
| public static String generateToken(IJWTInfo jwtInfo, byte priKey[], int expire) throws Exception { |
| String compactJws = Jwts.builder() |
| .setSubject(jwtInfo.getUniqueName()) |
| .claim(CommonConstants.JWT_KEY_USER_ID, jwtInfo.getId()) |
| .claim(CommonConstants.JWT_KEY_NAME, jwtInfo.getName()) |
| .claim(CommonConstants.JWT_ID, jwtInfo.getTokenId()) |
| .setExpiration(DateTime.now().plusSeconds(expire).toDate()) |
| .signWith(SignatureAlgorithm.RS256, rsaKeyHelper.getPrivateKey(priKey)) |
| .compact(); |
| return compactJws; |
| } |
密钥生成
| @Configuration |
| @Data |
| public class KeyConfiguration { |
| @Value("${jwt.rsa-secret}") |
| private String userSecret; |
| private byte[] userPubKey; |
| private byte[] userPriKey; |
| } |
项目启动时生成公钥和密钥并放到redis中
| |
| @Configuration |
| public class AuthServerRunner implements CommandLineRunner { |
| @Autowired |
| private RedisTemplate<String, String> redisTemplate; |
| private static final String REDIS_USER_PRI_KEY = "CLOUD_V1:AUTH:JWT:PRI"; |
| private static final String REDIS_USER_PUB_KEY = "CLOUD_V1:AUTH:JWT:PUB"; |
| |
| @Autowired |
| private KeyConfiguration keyConfiguration; |
| |
| @Override |
| public void run(String... args) throws Exception { |
| if (redisTemplate.hasKey(REDIS_USER_PRI_KEY)&&redisTemplate.hasKey(REDIS_USER_PUB_KEY)) { |
| keyConfiguration.setUserPriKey(RsaKeyHelper.toBytes(redisTemplate.opsForValue().get(REDIS_USER_PRI_KEY).toString())); |
| keyConfiguration.setUserPubKey(RsaKeyHelper.toBytes(redisTemplate.opsForValue().get(REDIS_USER_PUB_KEY).toString())); |
| } else { |
| Map<String, byte[]> keyMap = RsaKeyHelper.generateKey(keyConfiguration.getUserSecret()); |
| keyConfiguration.setUserPriKey(keyMap.get("pri")); |
| keyConfiguration.setUserPubKey(keyMap.get("pub")); |
| redisTemplate.opsForValue().set(REDIS_USER_PRI_KEY, RsaKeyHelper.toHexString(keyMap.get("pri"))); |
| redisTemplate.opsForValue().set(REDIS_USER_PUB_KEY, RsaKeyHelper.toHexString(keyMap.get("pub"))); |
| |
| } |
| } |
| } |
| |
根据 rsa-secret 生成公钥密钥
| |
| |
| |
| |
| |
| |
| |
| public static Map<String, byte[]> generateKey(String password) throws IOException, NoSuchAlgorithmException { |
| KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA"); |
| SecureRandom secureRandom = new SecureRandom(password.getBytes()); |
| keyPairGenerator.initialize(1024, secureRandom); |
| KeyPair keyPair = keyPairGenerator.genKeyPair(); |
| byte[] publicKeyBytes = keyPair.getPublic().getEncoded(); |
| byte[] privateKeyBytes = keyPair.getPrivate().getEncoded(); |
| Map<String, byte[]> map = new HashMap<String, byte[]>(); |
| map.put("pub", publicKeyBytes); |
| map.put("pri", privateKeyBytes); |
| return map; |
| } |
4.4创建当前会话并缓存到redis
| |
| |
| |
| |
| @Async |
| public void writeOnlineLog(JWTInfo jwtInfo) { |
| final UserAgent userAgent = UserAgent.parseUserAgentString(WebUtils.getRequest().getHeader("User-Agent")); |
| final String ip = IpUtils.getRemoteIP(WebUtils.getRequest()); |
| String address = AddressUtils.getRealAddressByIP(ip); |
| |
| OnlineLog onlineLog = new OnlineLog(); |
| |
| String os = userAgent.getOperatingSystem().getName(); |
| |
| String browser = userAgent.getBrowser().getName(); |
| onlineLog.setBrowser(browser); |
| onlineLog.setIpaddr(ip); |
| onlineLog.setTokenId(jwtInfo.getTokenId()); |
| onlineLog.setLoginTime(System.currentTimeMillis()); |
| onlineLog.setUserId(jwtInfo.getId()); |
| onlineLog.setUserName(jwtInfo.getName()); |
| onlineLog.setLoginLocation(address); |
| onlineLog.setOs(os); |
| |
| stringRedisTemplate.opsForValue().set(RedisKeyConstant.REDIS_KEY_TOKEN + ":" + jwtInfo.getTokenId(), JSON.toJSONString(onlineLog, false), expire, TimeUnit.MINUTES); |
| |
| stringRedisTemplate.opsForZSet().add((RedisKeyConstant.REDIS_KEY_TOKEN), jwtInfo.getTokenId(), 0); |
| } |
5.登录响应成功,跳转首页
| .then(async res => { |
| |
| |
| |
| |
| |
| util.cookies.set('uuid', res.id) |
| util.cookies.set('token', res.accessToken) |
| |
| await dispatch('d2admin/user/set', { |
| name: res.name |
| }, { root: true }) |
| |
| await dispatch('load') |
| |
| const path = util.cookies.get('redirect') |
| |
| vm.$router.replace(path ? { path } : route) |
| |
| util.cookies.remove('redirect') |
| }) |
| .catch(err => { |
| console.log('err: ', err) |
| }) |
登录流程大致图


【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· Manus的开源复刻OpenManus初探
· AI 智能体引爆开源社区「GitHub 热点速览」
· C#/.NET/.NET Core技术前沿周刊 | 第 29 期(2025年3.1-3.9)
· 从HTTP原因短语缺失研究HTTP/2和HTTP/3的设计差异