提交 05cb6097 编写于 作者: 查尔斯-BUG万象集's avatar 查尔斯-BUG万象集

feat: 支持第三方账号登录

Just Auth(开箱即用的整合第三方登录的开源组件,脱离繁琐的第三方登录 SDK,让登录变得 So easy!)
上级 71e20e9f
......@@ -72,7 +72,7 @@ ContiNew Admin (Continue New Admin)中后台管理框架/脚手架,持续
> **Note**
> 更多功能和优化正在赶来💦,最新项目计划和进展请关注 [GitHub Project](https://github.com/Charles7c/continew-admin/projects) 和 [CHANGELOG.md](https://github.com/Charles7c/continew-admin/blob/dev/CHANGELOG.md)。
- 用户管理:提供用户的相关配置,新增用户后,默认密码为 123456
- 用户管理:提供用户的相关配置,新增用户后,默认密码为 123456,且支持第三方账号登录
- 部门管理:可配置系统组织架构,树形表格展示
- 角色管理:对权限与菜单进行分配,可根据部门设置角色的数据权限
- 菜单管理:已实现菜单动态路由,后端可配置化,支持多级菜单
......@@ -235,6 +235,7 @@ pnpm dev
| <a href="https://github.com/liquibase/liquibase" target="_blank">Liquibase</a> | 4.9.1 | 用于管理数据库版本,跟踪、管理和应用数据库变化。 |
| <a href="https://redis.io/" target="_blank">Redis</a> | 6.2.7 | 高性能的 key-value 数据库。 |
| <a href="https://github.com/redisson/redisson/wiki/Redisson%E9%A1%B9%E7%9B%AE%E4%BB%8B%E7%BB%8D" target="_blank">Redisson</a> | 3.20.1 | 不仅仅是一个 Redis Java 客户端,同其他 Redis Java 客户端有着很大的区别,相比之下其他客户端提供的功能还仅仅停留在作为数据库驱动层面上,比如仅针对 Redis 提供连接方式,发送命令和处理返回结果等。而 Redisson 充分的利用了 Redis 键值数据库提供的一系列优势,基于 Java 实用工具包中常用接口,为使用者提供了一系列具有分布式特性的常用工具类。使得原本作为协调单机多线程并发程序的工具包获得了协调分布式多机多线程并发系统的能力,大大降低了设计和研发大规模分布式系统的难度。同时结合各富特色的分布式服务,更进一步简化了分布式环境中程序相互之间的协作。 |
| <a href="https://justauth.cn/" target="_blank">Just Auth</a> | 1.16.5 | 开箱即用的整合第三方登录的开源组件,脱离繁琐的第三方登录 SDK,让登录变得 So easy! |
| <a href="https://easyexcel.opensource.alibaba.com/" target="_blank">Easy Excel</a> | 3.3.2 | 一个基于 Java 的、快速、简洁、解决大文件内存溢出的 Excel 处理工具。 |
| Easy Captcha | 1.6.2 | Java 图形验证码,支持 gif、中文、算术等类型,可用于 Java Web、JavaSE 等项目。 |
| <a href="https://doc.xiaominfo.com/" target="_blank">Knife4j</a> | 4.3.0 | 前身是 swagger-bootstrap-ui,集 Swagger2 和 OpenAPI3 为一体的增强解决方案。本项目使用的是 <a href="https://gitee.com/xiaoym/swagger-bootstrap-ui-demo/tree/master/knife4j-springdoc-openapi-demo" target="_blank">knife4j-openapi3-spring-boot-starter</a> 基于 OpenAPI3 规范,在 Spring Boot < 3.0.0-M1 的单体架构下可以直接引用此 starter,该模块包含了 UI 部分,底层基于 springdoc-openapi 项目。 |
......
......@@ -115,6 +115,16 @@ limitations under the License.
</dependency>
<!-- ################ 工具库相关 ################ -->
<!-- Just Auth(开箱即用的整合第三方登录的开源组件,脱离繁琐的第三方登录 SDK,让登录变得 So easy!) -->
<dependency>
<groupId>com.xkcoding.justauth</groupId>
<artifactId>justauth-spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>me.zhyd.oauth</groupId>
<artifactId>JustAuth</artifactId>
</dependency>
<!-- Easy Excel(一个基于 Java 的、快速、简洁、解决大文件内存溢出的 Excel 处理工具) -->
<dependency>
<groupId>com.alibaba</groupId>
......
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.charles7c.cnadmin.common.config.justauth;
import java.time.Duration;
import top.charles7c.cnadmin.common.constant.CacheConsts;
import top.charles7c.cnadmin.common.util.RedisUtils;
import me.zhyd.oauth.cache.AuthStateCache;
/**
* Just Auth 自定义 State 缓存实现(Redis)
*
* @author Charles7c
* @since 2023/10/8 22:17
*/
public class JustAuthRedisStateCache implements AuthStateCache {
/**
* 存入缓存
*
* @param key
* 缓存 key
* @param value
* 缓存内容
*/
@Override
public void cache(String key, String value) {
// 参考:在 JustAuth 中,内置了一个基于 map 的 state 缓存器,默认缓存有效期为 3 分钟
RedisUtils.setCacheObject(RedisUtils.formatKey(CacheConsts.SOCIAL_AUTH_STATE_KEY_PREFIX, key), value,
Duration.ofMinutes(3));
}
/**
* 存入缓存
*
* @param key
* 缓存 key
* @param value
* 缓存内容
* @param timeout
* 指定缓存过期时间(毫秒)
*/
@Override
public void cache(String key, String value, long timeout) {
RedisUtils.setCacheObject(RedisUtils.formatKey(CacheConsts.SOCIAL_AUTH_STATE_KEY_PREFIX, key), value,
Duration.ofMillis(timeout));
}
/**
* 获取缓存内容
*
* @param key
* 缓存 key
* @return 缓存内容
*/
@Override
public String get(String key) {
return RedisUtils.getCacheObject(RedisUtils.formatKey(CacheConsts.SOCIAL_AUTH_STATE_KEY_PREFIX, key));
}
/**
* 是否存在 key,如果对应 key 的 value 值已过期,也返回 false
*
* @param key
* 缓存 key
* @return true:存在 key,并且 value 没过期;false:key 不存在或者已过期
*/
@Override
public boolean containsKey(String key) {
return RedisUtils.hasKey(RedisUtils.formatKey(CacheConsts.SOCIAL_AUTH_STATE_KEY_PREFIX, key));
}
}
\ No newline at end of file
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.charles7c.cnadmin.common.config.justauth;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import me.zhyd.oauth.cache.AuthStateCache;
/**
* Just Auth State 缓存配置
*
* @author Charles7c
* @since 2023/10/8 22:17
*/
@Configuration
public class JustAuthStateConfiguration {
/**
* Just Auth State 缓存 Redis 适配
*/
@Bean
public AuthStateCache authStateCache() {
return new JustAuthRedisStateCache();
}
}
\ No newline at end of file
......@@ -67,4 +67,9 @@ public class CacheConsts {
* 仪表盘缓存键前缀
*/
public static final String DASHBOARD_KEY_PREFIX = "DASHBOARD";
/**
* 社交身份认证状态键前缀
*/
public static final String SOCIAL_AUTH_STATE_KEY_PREFIX = "SOCIAL_AUTH_STATE";
}
......@@ -33,6 +33,11 @@ public class SysConsts {
*/
public static final String ADMIN_ROLE_CODE = "admin";
/**
* 顶级部门 ID
*/
public static final Long SUPER_DEPT_ID = 1L;
/**
* 顶级父 ID
*/
......@@ -53,6 +58,11 @@ public class SysConsts {
*/
public static final String LOGIN_URI = "/auth/login";
/**
* 退出 URI
*/
public static final String LOGOUT_URI = "/auth/logout";
/**
* VO 描述类字段后缀
*/
......
......@@ -53,8 +53,9 @@ public class LoginHelper {
*
* @param loginUser
* 登录用户信息
* @return 令牌
*/
public static void login(LoginUser loginUser) {
public static String login(LoginUser loginUser) {
// 记录登录信息
HttpServletRequest request = ServletUtils.getRequest();
loginUser.setClientIp(ServletUtil.getClientIP(request));
......@@ -65,8 +66,10 @@ public class LoginHelper {
// 登录并缓存用户信息
StpUtil.login(loginUser.getId());
SaHolder.getStorage().set(CacheConsts.LOGIN_USER_KEY, loginUser);
loginUser.setToken(StpUtil.getTokenValue());
String tokenValue = StpUtil.getTokenValue();
loginUser.setToken(tokenValue);
StpUtil.getTokenSession().set(CacheConsts.LOGIN_USER_KEY, loginUser);
return tokenValue;
}
/**
......
......@@ -215,11 +215,16 @@ public class LogInterceptor implements HandlerInterceptor {
private void logRequest(LogDO logDO, HttpServletRequest request) {
logDO.setRequestUrl(StrUtil.isBlank(request.getQueryString()) ? request.getRequestURL().toString()
: request.getRequestURL().append(StringConsts.QUESTION_MARK).append(request.getQueryString()).toString());
logDO.setRequestMethod(request.getMethod());
String method = request.getMethod();
logDO.setRequestMethod(method);
logDO.setRequestHeaders(this.desensitize(ServletUtil.getHeaderMap(request)));
String requestBody = this.getRequestBody(request);
logDO.setCreateUser(ObjectUtil.defaultIfNull(logDO.getCreateUser(), LoginHelper.getUserId()));
if (null == logDO.getCreateUser() && SysConsts.LOGIN_URI.equals(request.getRequestURI())) {
String requestURI = request.getRequestURI();
if (requestURI.startsWith("/auth") && !SysConsts.LOGOUT_URI.equals(requestURI)) {
logDO.setCreateUser(null);
}
if (null == logDO.getCreateUser() && SysConsts.LOGIN_URI.equals(requestURI)) {
LoginRequest loginRequest = JSONUtil.toBean(requestBody, LoginRequest.class);
logDO.setCreateUser(
ExceptionUtils.exToNull(() -> userService.getByUsername(loginRequest.getUsername()).getId()));
......
......@@ -20,6 +20,8 @@ import java.util.List;
import top.charles7c.cnadmin.auth.model.vo.RouteVO;
import me.zhyd.oauth.model.AuthUser;
/**
* 登录业务接口
*
......@@ -39,6 +41,15 @@ public interface LoginService {
*/
String login(String username, String password);
/**
* 社交身份登录
*
* @param authUser
* 社交身份信息
* @return 令牌
*/
String socialLogin(AuthUser authUser);
/**
* 构建路由树
*
......
......@@ -16,6 +16,7 @@
package top.charles7c.cnadmin.auth.service.impl;
import java.time.LocalDateTime;
import java.util.*;
import java.util.stream.Collectors;
......@@ -23,32 +24,38 @@ import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import cn.dev33.satoken.stp.StpUtil;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.lang.tree.Tree;
import cn.hutool.core.lang.tree.TreeNodeConfig;
import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.RandomUtil;
import cn.hutool.core.util.ReUtil;
import cn.hutool.json.JSONUtil;
import top.charles7c.cnadmin.auth.model.vo.MetaVO;
import top.charles7c.cnadmin.auth.model.vo.RouteVO;
import top.charles7c.cnadmin.auth.service.LoginService;
import top.charles7c.cnadmin.auth.service.PermissionService;
import top.charles7c.cnadmin.common.annotation.TreeField;
import top.charles7c.cnadmin.common.constant.RegexConsts;
import top.charles7c.cnadmin.common.constant.SysConsts;
import top.charles7c.cnadmin.common.enums.DisEnableStatusEnum;
import top.charles7c.cnadmin.common.enums.GenderEnum;
import top.charles7c.cnadmin.common.enums.MenuTypeEnum;
import top.charles7c.cnadmin.common.model.dto.LoginUser;
import top.charles7c.cnadmin.common.util.SecureUtils;
import top.charles7c.cnadmin.common.util.TreeUtils;
import top.charles7c.cnadmin.common.util.helper.LoginHelper;
import top.charles7c.cnadmin.common.util.validate.CheckUtils;
import top.charles7c.cnadmin.system.model.entity.RoleDO;
import top.charles7c.cnadmin.system.model.entity.UserDO;
import top.charles7c.cnadmin.system.model.entity.UserSocialDO;
import top.charles7c.cnadmin.system.model.vo.DeptDetailVO;
import top.charles7c.cnadmin.system.model.vo.MenuVO;
import top.charles7c.cnadmin.system.service.DeptService;
import top.charles7c.cnadmin.system.service.MenuService;
import top.charles7c.cnadmin.system.service.RoleService;
import top.charles7c.cnadmin.system.service.UserService;
import top.charles7c.cnadmin.system.service.*;
import me.zhyd.oauth.model.AuthUser;
/**
* 登录业务实现
......@@ -65,6 +72,8 @@ public class LoginServiceImpl implements LoginService {
private final RoleService roleService;
private final MenuService menuService;
private final PermissionService permissionService;
private final UserRoleService userRoleService;
private final UserSocialService userSocialService;
@Override
public String login(String username, String password) {
......@@ -72,16 +81,44 @@ public class LoginServiceImpl implements LoginService {
CheckUtils.throwIfNull(user, "用户名或密码错误");
Long userId = user.getId();
CheckUtils.throwIfNotEqual(SecureUtils.md5Salt(password, userId.toString()), user.getPassword(), "用户名或密码错误");
CheckUtils.throwIfEqual(DisEnableStatusEnum.DISABLE, user.getStatus(), "此账号已被禁用,如有疑问,请联系管理员");
DeptDetailVO deptDetailVO = deptService.get(user.getDeptId());
CheckUtils.throwIfEqual(DisEnableStatusEnum.DISABLE, deptDetailVO.getStatus(), "此账号部门已被禁用,如有疑问,请联系管理员");
// 登录并缓存用户信息
LoginUser loginUser = BeanUtil.copyProperties(user, LoginUser.class);
loginUser.setPermissions(permissionService.listPermissionByUserId(userId));
loginUser.setRoleCodes(permissionService.listRoleCodeByUserId(userId));
loginUser.setRoles(roleService.listByUserId(userId));
LoginHelper.login(loginUser);
return StpUtil.getTokenValue();
this.checkUserStatus(user);
return this.login(user);
}
@Override
public String socialLogin(AuthUser authUser) {
String source = authUser.getSource();
String openId = authUser.getUuid();
UserSocialDO userSocial = userSocialService.getBySourceAndOpenId(source, openId);
UserDO user;
if (null == userSocial) {
String username = authUser.getUsername();
boolean isMatch = ReUtil.isMatch(RegexConsts.USERNAME, username);
UserDO existsUser = userService.getByUsername(username);
if (null != existsUser || !isMatch) {
username = RandomUtil.randomString(RandomUtil.BASE_CHAR, 5) + IdUtil.fastSimpleUUID();
}
user = new UserDO();
user.setUsername(username);
user.setNickname(authUser.getNickname());
user.setGender(GenderEnum.valueOf(authUser.getGender().name()));
user.setAvatar(authUser.getAvatar());
user.setDeptId(SysConsts.SUPER_DEPT_ID);
Long userId = userService.save(user);
RoleDO role = roleService.getByCode(SysConsts.ADMIN_ROLE_CODE);
userRoleService.save(Collections.singletonList(role.getId()), userId);
userSocial = new UserSocialDO();
userSocial.setUserId(userId);
userSocial.setSource(source);
userSocial.setOpenId(openId);
} else {
user = BeanUtil.toBean(userService.get(userSocial.getUserId()), UserDO.class);
}
this.checkUserStatus(user);
userSocial.setMetaJson(JSONUtil.toJsonStr(authUser));
userSocial.setLastLoginTime(LocalDateTime.now());
userSocialService.saveOrUpdate(userSocial);
return this.login(user);
}
@Override
......@@ -120,4 +157,32 @@ public class LoginServiceImpl implements LoginService {
});
return BeanUtil.copyToList(treeList, RouteVO.class);
}
/**
* 登录并缓存用户信息
*
* @param user
* 用户信息
* @return 令牌
*/
private String login(UserDO user) {
Long userId = user.getId();
LoginUser loginUser = BeanUtil.copyProperties(user, LoginUser.class);
loginUser.setPermissions(permissionService.listPermissionByUserId(userId));
loginUser.setRoleCodes(permissionService.listRoleCodeByUserId(userId));
loginUser.setRoles(roleService.listByUserId(userId));
return LoginHelper.login(loginUser);
}
/**
* 检查用户状态
*
* @param user
* 用户信息
*/
private void checkUserStatus(UserDO user) {
CheckUtils.throwIfEqual(DisEnableStatusEnum.DISABLE, user.getStatus(), "此账号已被禁用,如有疑问,请联系管理员");
DeptDetailVO deptDetailVO = deptService.get(user.getDeptId());
CheckUtils.throwIfEqual(DisEnableStatusEnum.DISABLE, deptDetailVO.getStatus(), "此账号部门已被禁用,如有疑问,请联系管理员");
}
}
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.charles7c.cnadmin.system.mapper;
import org.apache.ibatis.annotations.Param;
import top.charles7c.cnadmin.common.base.BaseMapper;
import top.charles7c.cnadmin.system.model.entity.UserSocialDO;
/**
* 用户社会化关联 Mapper
*
* @author Charles7c
* @since 2023/10/11 22:10
*/
public interface UserSocialMapper extends BaseMapper<UserSocialDO> {
/**
* 根据来源和开放 ID 查询
*
* @param source
* 来源
* @param openId
* 开放 ID
* @return 用户社会化关联信息
*/
UserSocialDO selectBySourceAndOpenId(@Param("source") String source, @Param("openId") String openId);
}
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.charles7c.cnadmin.system.model.entity;
import java.io.Serializable;
import java.time.LocalDateTime;
import lombok.Data;
import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableName;
/**
* 用户社会化关联实体
*
* @author Charles7c
* @since 2023/10/11 22:10
*/
@Data
@TableName("sys_user_social")
public class UserSocialDO implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 用户 ID
*/
private Long userId;
/**
* 来源
*/
private String source;
/**
* 开放 ID
*/
private String openId;
/**
* 附加信息
*/
private String metaJson;
/**
* 最后登录时间
*/
private LocalDateTime lastLoginTime;
/**
* 创建时间
*/
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createTime;
}
......@@ -22,6 +22,7 @@ import java.util.Set;
import top.charles7c.cnadmin.common.base.BaseService;
import top.charles7c.cnadmin.common.model.dto.RoleDTO;
import top.charles7c.cnadmin.common.model.vo.LabelValueVO;
import top.charles7c.cnadmin.system.model.entity.RoleDO;
import top.charles7c.cnadmin.system.model.query.RoleQuery;
import top.charles7c.cnadmin.system.model.request.RoleRequest;
import top.charles7c.cnadmin.system.model.vo.RoleDetailVO;
......@@ -70,4 +71,13 @@ public interface RoleService extends BaseService<RoleVO, RoleDetailVO, RoleQuery
* @return 角色集合
*/
Set<RoleDTO> listByUserId(Long userId);
/**
* 根据角色编码查询
*
* @param code
* 角色编码
* @return 角色信息
*/
RoleDO getByCode(String code);
}
......@@ -37,6 +37,15 @@ import top.charles7c.cnadmin.system.model.vo.UserVO;
*/
public interface UserService extends BaseService<UserVO, UserDetailVO, UserQuery, UserRequest> {
/**
* 保存用户信息
*
* @param user
* 用户信息
* @return ID
*/
Long save(UserDO user);
/**
* 上传头像
*
......
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.charles7c.cnadmin.system.service;
import top.charles7c.cnadmin.system.model.entity.UserSocialDO;
/**
* 用户社会化关联业务接口
*
* @author Charles7c
* @since 2023/10/11 22:10
*/
public interface UserSocialService {
/**
* 根据来源和开放 ID 查询
*
* @param source
* 来源
* @param openId
* 开放 ID
* @return 用户社会化关联信息
*/
UserSocialDO getBySourceAndOpenId(String source, String openId);
/**
* 保存
*
* @param userSocial
* 用户社会化关联信息
*/
void saveOrUpdate(UserSocialDO userSocial);
}
\ No newline at end of file
......@@ -178,6 +178,11 @@ public class RoleServiceImpl extends BaseServiceImpl<RoleMapper, RoleDO, RoleVO,
return new HashSet<>(BeanUtil.copyToList(roleList, RoleDTO.class));
}
@Override
public RoleDO getByCode(String code) {
return baseMapper.lambdaQuery().eq(RoleDO::getCode, code).one();
}
/**
* 检查名称是否存在
*
......
......@@ -18,9 +18,7 @@ package top.charles7c.cnadmin.system.service.impl;
import java.io.File;
import java.time.LocalDateTime;
import java.util.Collection;
import java.util.List;
import java.util.Optional;
import java.util.*;
import javax.annotation.Resource;
......@@ -82,6 +80,13 @@ public class UserServiceImpl extends BaseServiceImpl<UserMapper, UserDO, UserVO,
@Resource
private DeptService deptService;
@Override
public Long save(UserDO user) {
user.setStatus(DisEnableStatusEnum.ENABLE);
baseMapper.insert(user);
return user.getId();
}
@Override
@Transactional(rollbackFor = Exception.class)
public Long add(UserRequest request) {
......
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.charles7c.cnadmin.system.service.impl;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import top.charles7c.cnadmin.system.mapper.UserSocialMapper;
import top.charles7c.cnadmin.system.model.entity.UserSocialDO;
import top.charles7c.cnadmin.system.service.UserSocialService;
/**
* 用户社会化关联业务实现
*
* @author Charles7c
* @since 2023/10/11 22:10
*/
@Service
@RequiredArgsConstructor
public class UserSocialServiceImpl implements UserSocialService {
private final UserSocialMapper baseMapper;
@Override
public UserSocialDO getBySourceAndOpenId(String source, String openId) {
return baseMapper.selectBySourceAndOpenId(source, openId);
}
@Override
@Transactional(rollbackFor = Exception.class)
public void saveOrUpdate(UserSocialDO userSocial) {
if (null == userSocial.getCreateTime()) {
baseMapper.insert(userSocial);
} else {
baseMapper.lambdaUpdate().set(UserSocialDO::getMetaJson, userSocial.getMetaJson())
.set(UserSocialDO::getLastLoginTime, userSocial.getLastLoginTime())
.eq(UserSocialDO::getSource, userSocial.getSource()).eq(UserSocialDO::getOpenId, userSocial.getOpenId())
.update();
}
}
}
<?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="top.charles7c.cnadmin.system.mapper.UserSocialMapper">
<select id="selectBySourceAndOpenId"
resultType="top.charles7c.cnadmin.system.model.entity.UserSocialDO">
SELECT t1.*
FROM `sys_user_social` AS t1
LEFT JOIN `sys_user` AS t2 ON t2.`id` = t1.`user_id`
WHERE t1.`source` = #{source} AND t1.`open_id` = #{openId}
</select>
</mapper>
\ No newline at end of file
......@@ -32,3 +32,11 @@ export function getUserInfo() {
export function listRoute() {
return axios.get<RouteRecordNormalized[]>(`${BASE_URL}/route`);
}
export function socialAuth(source: string) {
return axios.get<string>(`${BASE_URL}/${source}`);
}
export function socialLogin(source: string, req: any) {
return axios.post<LoginRes>(`${BASE_URL}/${source}`, req);
}
export const WHITE_LIST = [
{ name: 'notFound', children: [] },
{ name: 'login', children: [] },
{ name: 'SocialCallback', children: [] },
];
export const NOT_FOUND = {
......
......@@ -29,7 +29,7 @@ export default function setupUserLoginInfoGuard(router: Router) {
}
}
} else {
if (to.name === 'login') {
if (to.name === 'login' || to.name === 'SocialCallback') {
next();
return;
}
......
......@@ -29,6 +29,14 @@ const router = createRouter({
requiresAuth: false,
},
},
{
path: '/social/callback',
name: 'SocialCallback',
component: () => import('@/views/login/social/index.vue'),
meta: {
requiresAuth: false,
},
},
...appRoutes,
...fixedRoutes,
...demoRoutes,
......
import { defineStore } from 'pinia';
import {
login as userLogin,
socialLogin as userSocialLogin,
logout as userLogout,
getUserInfo,
LoginReq,
......@@ -52,6 +53,17 @@ const useLoginStore = defineStore('user', {
}
},
// 社交身份登录
async socialLogin(source: string, req: any) {
try {
const res = await userSocialLogin(source, req);
setToken(res.data.token);
} catch (err) {
clearToken();
throw err;
}
},
// 用户退出
async logout() {
try {
......
......@@ -8,7 +8,14 @@ export default function getAvatar(
) {
if (avatar) {
const baseUrl = import.meta.env.VITE_API_BASE_URL;
return `${baseUrl}/avatar/${avatar}`;
if (
!avatar.startsWith('http://') &&
!avatar.startsWith('https://') &&
!avatar.startsWith('blob:')
) {
return `${baseUrl}/avatar/${avatar}`;
}
return avatar;
}
if (gender === 1) {
......
......@@ -11,21 +11,15 @@
<div class="container">
<div class="left-banner"></div>
<div class="login-card">
<div class="title"
>{{ $t('login.welcome') }} {{ appStore.getTitle }}</div
>
<div class="title">
{{ $t('login.welcome') }} {{ appStore.getTitle }}
</div>
<EmailLogin v-if="isEmailLogin" />
<a-tabs v-else class="account-tab" default-active-key="1">
<a-tab-pane
key="1"
:title="$t('login.account')"
>
<a-tab-pane key="1" :title="$t('login.account')">
<AccountLogin />
</a-tab-pane>
<a-tab-pane
key="2"
:title="$t('login.phone')"
>
<a-tab-pane key="2" :title="$t('login.phone')">
<PhoneLogin />
</a-tab-pane>
</a-tabs>
......@@ -40,8 +34,8 @@
<div v-else class="account app" @click="toggleLoginMode">
<icon-user /> {{ $t('login.account.txt') }}
</div>
<a-tooltip content="Gitee(即将开放)" mini>
<a href="javascript: void(0);" class="app">
<a-tooltip content="Gitee" mini>
<a-link class="app" @click="handleSocialAuth('gitee')">
<svg
class="icon"
fill="#C71D23"
......@@ -53,10 +47,10 @@
d="M11.984 0A12 12 0 0 0 0 12a12 12 0 0 0 12 12 12 12 0 0 0 12-12A12 12 0 0 0 12 0a12 12 0 0 0-.016 0zm6.09 5.333c.328 0 .593.266.592.593v1.482a.594.594 0 0 1-.593.592H9.777c-.982 0-1.778.796-1.778 1.778v5.63c0 .327.266.592.593.592h5.63c.982 0 1.778-.796 1.778-1.778v-.296a.593.593 0 0 0-.592-.593h-4.15a.592.592 0 0 1-.592-.592v-1.482a.593.593 0 0 1 .593-.592h6.815c.327 0 .593.265.593.592v3.408a4 4 0 0 1-4 4H5.926a.593.593 0 0 1-.593-.593V9.778a4.444 4.444 0 0 1 4.445-4.444h8.296Z"
/>
</svg>
</a>
</a-link>
</a-tooltip>
<a-tooltip content="GitHub(即将开放)" mini>
<a href="javascript: void(0);" class="app">
<a-tooltip content="GitHub" mini>
<a-link class="app" @click="handleSocialAuth('github')">
<svg
class="icon"
role="img"
......@@ -67,7 +61,7 @@
d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12"
/>
</svg>
</a>
</a-link>
</a-tooltip>
</div>
</div>
......@@ -87,6 +81,7 @@
import { useAppStore } from '@/store';
import getFile from '@/utils/file';
import useResponsive from '@/hooks/responsive';
import { socialAuth } from '@/api/auth/login';
import AccountLogin from './components/account-login.vue';
import PhoneLogin from './components/phone-login.vue';
import EmailLogin from './components/email-login.vue';
......@@ -96,6 +91,16 @@
useResponsive(true);
const isEmailLogin = ref(false);
/**
* 第三方登录授权
*
* @param source 来源
*/
const handleSocialAuth = async (source: string) => {
const { data } = await socialAuth(source);
window.location.href = data;
};
const toggleLoginMode = () => {
isEmailLogin.value = !isEmailLogin.value;
};
......
......@@ -4,6 +4,7 @@ export default {
'login.phone': 'Phone Login',
'login.email': 'Email Login',
'login.other': 'Other Login',
'login.ing': 'Login...',
'login.account.placeholder.username': 'Please enter username',
'login.account.placeholder.password': 'Please enter password',
......
......@@ -4,6 +4,7 @@ export default {
'login.phone': '手机号登录',
'login.email': '邮箱登录',
'login.other': '其他登录方式',
'login.ing': '登录中...',
'login.account.placeholder.username': '请输入用户名',
'login.account.placeholder.password': '请输入密码',
......
<template>
<a-spin :loading="loading" :tip="$t('login.ing')">
<div></div>
</a-spin>
</template>
<script setup lang="ts">
import { getCurrentInstance, ref } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { useLoginStore } from '@/store';
import { useI18n } from 'vue-i18n';
const { proxy } = getCurrentInstance() as any;
const { t } = useI18n();
const route = useRoute();
const router = useRouter();
const loginStore = useLoginStore();
const loading = ref(false);
const source = route.query.source as string;
/**
* 社会化身份登录
*/
const handleSocialLogin = () => {
if (loading.value) return;
loading.value = true;
const { redirect, ...othersQuery } = router.currentRoute.value.query;
loginStore
.socialLogin(source, othersQuery)
.then(() => {
router.push({
name: (redirect as string) || 'Workplace',
});
proxy.$notification.success(t('login.success'));
})
.catch(() => {
router.push({
name: 'login',
query: {
...othersQuery,
},
});
})
.finally(() => {
loading.value = false;
});
};
handleSocialLogin();
</script>
<script lang="ts">
export default {
name: 'SocialCallback',
};
</script>
<style scoped lang="less">
div {
width: 150px;
height: 150px;
position: absolute;
left: 50%;
top: 45%;
margin-left: -50px;
margin-top: -50px;
}
</style>
......@@ -147,7 +147,7 @@
>
<template #columns>
<a-table-column title="ID" data-index="id" />
<a-table-column title="用户名" :width="115">
<a-table-column title="用户名" :width="120" ellipsis tooltip>
<template #cell="{ record }">
<a-link @click="toDetail(record.id)">{{
record.username
......
......@@ -49,16 +49,17 @@ import top.charles7c.cnadmin.system.model.vo.UserDetailVO;
import top.charles7c.cnadmin.system.service.UserService;
/**
* 登录 API
* 认证 API
*
* @author Charles7c
* @since 2022/12/21 20:37
*/
@Tag(name = "登录 API")
@Log(module = "登录")
@Tag(name = "认证 API")
@RestController
@RequiredArgsConstructor
@RequestMapping("/auth")
public class LoginController {
public class AuthController {
private final LoginService loginService;
private final UserService userService;
......
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.charles7c.cnadmin.webapi.controller.auth;
import lombok.RequiredArgsConstructor;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.enums.ParameterIn;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.web.bind.annotation.*;
import com.xkcoding.justauth.AuthRequestFactory;
import cn.dev33.satoken.annotation.SaIgnore;
import top.charles7c.cnadmin.auth.model.vo.LoginVO;
import top.charles7c.cnadmin.auth.service.LoginService;
import top.charles7c.cnadmin.common.exception.BadRequestException;
import top.charles7c.cnadmin.common.model.vo.R;
import top.charles7c.cnadmin.common.util.validate.ValidationUtils;
import top.charles7c.cnadmin.monitor.annotation.Log;
import me.zhyd.oauth.model.AuthCallback;
import me.zhyd.oauth.model.AuthResponse;
import me.zhyd.oauth.model.AuthUser;
import me.zhyd.oauth.request.AuthRequest;
import me.zhyd.oauth.utils.AuthStateUtils;
/**
* 社交身份认证 API
*
* @author Charles7c
* @since 2023/10/8 22:52
*/
@Log(module = "登录")
@Tag(name = "社交身份认证 API")
@SaIgnore
@RestController
@RequiredArgsConstructor
@RequestMapping("/auth")
public class SocialAuthController {
private final LoginService loginService;
private final AuthRequestFactory authRequestFactory;
@Operation(summary = "社交身份登录授权", description = "社交身份登录授权")
@Parameter(name = "source", description = "来源", example = "gitee", in = ParameterIn.PATH)
@GetMapping("/{source}")
public R authorize(@PathVariable String source) {
AuthRequest authRequest = this.getAuthRequest(source);
return R.ok("操作成功", authRequest.authorize(AuthStateUtils.createState()));
}
@Operation(summary = "社交身份登录", description = "社交身份登录")
@Parameter(name = "source", description = "来源", example = "gitee", in = ParameterIn.PATH)
@PostMapping("/{source}")
public LoginVO login(@PathVariable String source, @RequestBody AuthCallback callback) {
AuthRequest authRequest = this.getAuthRequest(source);
AuthResponse<AuthUser> response = authRequest.login(callback);
ValidationUtils.throwIf(!response.ok(), response.getMsg());
AuthUser authUser = response.getData();
String token = loginService.socialLogin(authUser);
return LoginVO.builder().token(token).build();
}
private AuthRequest getAuthRequest(String source) {
try {
return authRequestFactory.get(source);
} catch (Exception e) {
throw new BadRequestException(String.format("暂不支持 [%s] 登录", source));
}
}
}
\ No newline at end of file
--- ### 项目配置
project:
# URL
url: http://localhost:5173
--- ### 服务器配置
server:
# HTTP 端口(默认 8080)
......@@ -73,6 +78,21 @@ spring:
# 是否允许缓存空值(默认 true,表示允许,可以解决缓存穿透问题)
cache-null-values: true
--- ### Just Auth 配置
justauth:
enabled: true
type:
GITEE:
client-id: 5d271b7f638941812aaf8bfc2e2f08f06d6235ef934e0e39537e2364eb8452c4
client-secret: 1f7d08**********5b7**********29e
redirect-uri: ${project.url}/social/callback?source=gitee
GITHUB:
client-id: 38080dad08cfbdfacca9
client-secret: 1f7d08**********5b7**********29e
redirect-uri: ${project.url}/social/callback?source=github
cache:
type: custom
--- ### 邮件配置
spring.mail:
# 根据需要更换
......
--- ### 项目配置
project:
# URL
url: https://cnadmin.charles7c.top
--- ### 服务器配置
server:
# HTTP 端口(默认 8080)
......@@ -73,6 +78,21 @@ spring:
# 是否允许缓存空值(默认 true,表示允许,可以解决缓存穿透问题)
cache-null-values: true
--- ### Just Auth 配置
justauth:
enabled: true
type:
GITEE:
client-id: 5d271b7f638941812aaf8bfc2e2f08f06d6235ef934e0e39537e2364eb8452c4
client-secret: 1f7d08**********5b7**********29e
redirect-uri: ${project.url}/social/callback?source=gitee
GITHUB:
client-id: 38080dad08cfbdfacca9
client-secret: 1f7d08**********5b7**********29e
redirect-uri: ${project.url}/social/callback?source=github
cache:
type: custom
--- ### 邮件配置
spring.mail:
# 根据需要更换
......
......@@ -8,8 +8,6 @@ project:
version: 1.3.0-SNAPSHOT
# 描述
description: ContiNew Admin 中后台管理框架/脚手架,Continue New Admin,持续以最新流行技术栈构建,拥抱变化,迭代优化。
# URL
url: https://cnadmin.charles7c.top
# 基本包
basePackage: top.charles7c.cnadmin
## 作者信息配置
......
......@@ -17,3 +17,9 @@ databaseChangeLog:
file: db/changelog/v1.2.0/continew-admin_column.sql
- include:
file: db/changelog/v1.2.0/continew-admin_data.sql
- include:
file: db/changelog/v1.3.0/continew-admin_table.sql
- include:
file: db/changelog/v1.3.0/continew-admin_column.sql
- include:
file: db/changelog/v1.3.0/continew-admin_data.sql
......@@ -93,7 +93,7 @@ CREATE TABLE IF NOT EXISTS `sys_user` (
`is_system` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否为系统内置数据',
`pwd_reset_time` datetime DEFAULT NULL COMMENT '最后一次修改密码时间',
`dept_id` bigint(20) NOT NULL COMMENT '部门ID',
`create_user` bigint(20) NOT NULL COMMENT '创建人',
`create_user` bigint(20) DEFAULT NULL COMMENT '创建人',
`create_time` datetime NOT NULL COMMENT '创建时间',
`update_user` bigint(20) DEFAULT NULL COMMENT '修改人',
`update_time` datetime DEFAULT NULL COMMENT '修改时间',
......
-- liquibase formatted sql
-- changeset Charles7c:1
CREATE TABLE IF NOT EXISTS `sys_user_social` (
`source` varchar(255) NOT NULL COMMENT '来源',
`open_id` varchar(255) NOT NULL COMMENT '开放ID',
`user_id` bigint(20) NOT NULL COMMENT '用户ID',
`meta_json` text DEFAULT NULL COMMENT '附加信息',
`last_login_time` datetime DEFAULT NULL COMMENT '最后登录时间',
`create_time` datetime NOT NULL COMMENT '创建时间',
UNIQUE INDEX `uk_source_open_id`(`source`, `open_id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户社会化关联表';
\ No newline at end of file
......@@ -52,6 +52,7 @@ limitations under the License.
<p6spy.version>3.9.1</p6spy.version>
<!-- ### 工具库相关 ### -->
<justauth.version>1.16.5</justauth.version>
<easyexcel.version>3.3.2</easyexcel.version>
<ip2region.version>2.7.15</ip2region.version>
<knife4j.version>4.3.0</knife4j.version>
......@@ -115,6 +116,28 @@ limitations under the License.
</dependency>
<!-- ################ 工具库相关 ################ -->
<!-- Just Auth(开箱即用的整合第三方登录的开源组件,脱离繁琐的第三方登录 SDK,让登录变得 So easy!) -->
<dependency>
<groupId>com.xkcoding.justauth</groupId>
<artifactId>justauth-spring-boot-starter</artifactId>
<version>1.4.0</version>
<exclusions>
<exclusion>
<groupId>cn.hutool</groupId>
<artifactId>hutool-core</artifactId>
</exclusion>
<exclusion>
<groupId>me.zhyd.oauth</groupId>
<artifactId>JustAuth</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>me.zhyd.oauth</groupId>
<artifactId>JustAuth</artifactId>
<version>${justauth.version}</version>
</dependency>
<!-- Easy Excel(一个基于 Java 的、快速、简洁、解决大文件内存溢出的 Excel 处理工具) -->
<dependency>
<groupId>com.alibaba</groupId>
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册