diff --git a/bookMarkService/business/user/src/main/java/com/fanxb/bookmark/business/user/controller/UserController.java b/bookMarkService/business/user/src/main/java/com/fanxb/bookmark/business/user/controller/UserController.java index 8f7d04c..31e68df 100644 --- a/bookMarkService/business/user/src/main/java/com/fanxb/bookmark/business/user/controller/UserController.java +++ b/bookMarkService/business/user/src/main/java/com/fanxb/bookmark/business/user/controller/UserController.java @@ -1,9 +1,12 @@ package com.fanxb.bookmark.business.user.controller; import com.alibaba.fastjson.JSONObject; -import com.fanxb.bookmark.business.user.vo.LoginBody; -import com.fanxb.bookmark.business.user.vo.RegisterBody; +import com.fanxb.bookmark.business.user.service.OAuthService; import com.fanxb.bookmark.business.user.service.UserService; +import com.fanxb.bookmark.business.user.vo.LoginBody; +import com.fanxb.bookmark.business.user.vo.OAuthBody; +import com.fanxb.bookmark.business.user.vo.RegisterBody; +import com.fanxb.bookmark.business.user.service.impl.UserServiceImpl; import com.fanxb.bookmark.common.entity.Result; import com.fanxb.bookmark.common.util.UserContextHolder; import org.apache.ibatis.annotations.Param; @@ -22,8 +25,16 @@ import org.springframework.web.multipart.MultipartFile; @RequestMapping("/user") public class UserController { + private final UserServiceImpl userServiceImpl; + private final OAuthService oAuthService; + private final UserService userService; + @Autowired - private UserService userService; + public UserController(UserServiceImpl userServiceImpl, OAuthService oAuthService, UserService userService) { + this.userServiceImpl = userServiceImpl; + this.oAuthService = oAuthService; + this.userService = userService; + } /** * Description: 获取验证码 @@ -35,7 +46,7 @@ public class UserController { */ @GetMapping("/authCode") public Result getAuthCode(@Param("email") String email) { - userService.sendAuthCode(email); + userServiceImpl.sendAuthCode(email); return Result.success(null); } @@ -49,7 +60,7 @@ public class UserController { */ @PutMapping("") public Result register(@RequestBody RegisterBody body) { - return Result.success(userService.register(body)); + return Result.success(userServiceImpl.register(body)); } /** @@ -61,7 +72,7 @@ public class UserController { */ @GetMapping("/currentUserInfo") public Result currentUserInfo() { - return Result.success(userService.getUserInfo(UserContextHolder.get().getUserId())); + return Result.success(userServiceImpl.getUserInfo(UserContextHolder.get().getUserId())); } @@ -72,7 +83,7 @@ public class UserController { */ @PostMapping("/icon") public Result pushIcon(@RequestParam("file") MultipartFile file) throws Exception { - return Result.success(userService.updateIcon(file)); + return Result.success(userServiceImpl.updateIcon(file)); } /** @@ -85,7 +96,7 @@ public class UserController { */ @PostMapping("/login") public Result login(@RequestBody LoginBody body) { - return Result.success(userService.login(body)); + return Result.success(userServiceImpl.login(body)); } /** @@ -98,7 +109,7 @@ public class UserController { */ @PostMapping("/resetPassword") public Result resetPassword(@RequestBody RegisterBody body) { - userService.resetPassword(body); + userServiceImpl.resetPassword(body); return Result.success(null); } @@ -112,7 +123,7 @@ public class UserController { */ @PostMapping("/checkPassword") public Result checkPassword(@RequestBody JSONObject obj) { - return Result.success(userService.checkPassword(obj.getString("password"))); + return Result.success(userServiceImpl.checkPassword(obj.getString("password"))); } @GetMapping("/loginStatus") @@ -120,5 +131,28 @@ public class UserController { return Result.success(null); } + /** + * 第三方登陆 + * + * @param body 入参 + * @return com.fanxb.bookmark.common.entity.Result + * @author fanxb + * @date 2021/3/10 + */ + @PostMapping("oAuthLogin") + public Result oAuthLogin(@RequestBody OAuthBody body) { + return Result.success(oAuthService.oAuthCheck(body)); + } + + /** + * 获取用户version + * + * @date 2021/3/11 + **/ + @GetMapping("/version") + public Result getUserVersion() { + return Result.success(userService.getCurrentUserVersion(UserContextHolder.get().getUserId())); + } + } diff --git a/bookMarkService/business/user/src/main/java/com/fanxb/bookmark/business/user/dao/UserDao.java b/bookMarkService/business/user/src/main/java/com/fanxb/bookmark/business/user/dao/UserDao.java index 0afcab5..b925792 100644 --- a/bookMarkService/business/user/src/main/java/com/fanxb/bookmark/business/user/dao/UserDao.java +++ b/bookMarkService/business/user/src/main/java/com/fanxb/bookmark/business/user/dao/UserDao.java @@ -3,6 +3,7 @@ package com.fanxb.bookmark.business.user.dao; import com.fanxb.bookmark.common.entity.User; import com.fanxb.bookmark.common.entity.redis.UserBookmarkUpdate; import org.apache.ibatis.annotations.Param; +import org.apache.ibatis.annotations.Select; import org.apache.ibatis.annotations.Update; import org.springframework.stereotype.Component; @@ -64,7 +65,7 @@ public interface UserDao { * @author fanxb * @date 2019/7/30 16:08 */ - User selectByUserId(int userId); + User selectByUserIdOrGithubId(@Param("userId") Integer userId, @Param("githubId") Long githubId); /** * Description: 更新用户icon @@ -102,11 +103,11 @@ public interface UserDao { /** * 更新用户新邮箱 * - * @param userId userId - * @param newPassword userId + * @param userId userId + * @param newEmail email */ - @Update("update user set newEmail=#{newPassword} where userId= #{userId}") - void updateNewEmailByUserId(@Param("userId") int userId, @Param("newPassword") String newPassword); + @Update("update user set newEmail=#{newEmail} where userId= #{userId}") + void updateNewEmailByUserId(@Param("userId") int userId, @Param("newEmail") String newEmail); /** * 新邮箱校验成功,更新邮箱 @@ -135,4 +136,36 @@ public interface UserDao { */ @Update("update user set version=version+1") void updateAllBookmarkUpdateVersion(); + + /** + * 判断用户名是否存在 + * + * @param name name + * @return boolean + * @author fanxb + * @date 2021/3/11 + **/ + @Select("select count(1) from user where username=#{name}") + boolean usernameExist(String name); + + /** + * 更新githubId + * + * @param user user + * @author fanxb + * @date 2021/3/11 + **/ + @Update("update user set githubId=#{githubId},email=#{email} where userId=#{userId}") + void updateEmailAndGithubId(User user); + + /** + * 获取用户版本 + * + * @param userId userId + * @return int + * @author fanxb + * @date 2021/3/11 + **/ + @Select("select version from user where userId=#{userId}") + int getUserVersion(int userId); } diff --git a/bookMarkService/business/user/src/main/java/com/fanxb/bookmark/business/user/service/BaseInfoService.java b/bookMarkService/business/user/src/main/java/com/fanxb/bookmark/business/user/service/BaseInfoService.java index 6afaec6..8b14da3 100644 --- a/bookMarkService/business/user/src/main/java/com/fanxb/bookmark/business/user/service/BaseInfoService.java +++ b/bookMarkService/business/user/src/main/java/com/fanxb/bookmark/business/user/service/BaseInfoService.java @@ -37,7 +37,7 @@ public class BaseInfoService { public void changePassword(UpdatePasswordBody body) { int userId = UserContextHolder.get().getUserId(); - String password = userDao.selectByUserId(userId).getPassword(); + String password = userDao.selectByUserIdOrGithubId(userId, null).getPassword(); if (!StrUtil.equals(password, HashUtil.getPassword(body.getOldPassword()))) { throw new CustomException("旧密码错误"); } @@ -66,7 +66,7 @@ public class BaseInfoService { @Transactional(rollbackFor = Exception.class) public void updateEmail(EmailUpdateBody body) { int userId = UserContextHolder.get().getUserId(); - String oldPassword = userDao.selectByUserId(userId).getPassword(); + String oldPassword = userDao.selectByUserIdOrGithubId(userId, null).getPassword(); if (!StrUtil.equals(oldPassword, HashUtil.getPassword(body.getOldPassword()))) { throw new CustomException("密码校验失败,无法更新email"); } diff --git a/bookMarkService/business/user/src/main/java/com/fanxb/bookmark/business/user/service/OAuthService.java b/bookMarkService/business/user/src/main/java/com/fanxb/bookmark/business/user/service/OAuthService.java index bb8e988..a7780ef 100644 --- a/bookMarkService/business/user/src/main/java/com/fanxb/bookmark/business/user/service/OAuthService.java +++ b/bookMarkService/business/user/src/main/java/com/fanxb/bookmark/business/user/service/OAuthService.java @@ -1,5 +1,8 @@ package com.fanxb.bookmark.business.user.service; -public class OAuthService { +import com.fanxb.bookmark.business.user.vo.OAuthBody; +public interface OAuthService { + + String oAuthCheck(OAuthBody body); } diff --git a/bookMarkService/business/user/src/main/java/com/fanxb/bookmark/business/user/service/UserService.java b/bookMarkService/business/user/src/main/java/com/fanxb/bookmark/business/user/service/UserService.java index 3dbf4bd..316e11f 100644 --- a/bookMarkService/business/user/src/main/java/com/fanxb/bookmark/business/user/service/UserService.java +++ b/bookMarkService/business/user/src/main/java/com/fanxb/bookmark/business/user/service/UserService.java @@ -1,219 +1,43 @@ -package com.fanxb.bookmark.business.user.service; - -import com.fanxb.bookmark.business.user.constant.FileConstant; -import com.fanxb.bookmark.business.user.dao.UserDao; -import com.fanxb.bookmark.business.user.vo.LoginBody; -import com.fanxb.bookmark.business.user.vo.LoginRes; -import com.fanxb.bookmark.business.user.vo.RegisterBody; -import com.fanxb.bookmark.common.constant.Constant; -import com.fanxb.bookmark.common.constant.NumberConstant; -import com.fanxb.bookmark.common.constant.RedisConstant; -import com.fanxb.bookmark.common.entity.MailInfo; -import com.fanxb.bookmark.common.entity.User; -import com.fanxb.bookmark.common.exception.FormDataException; -import com.fanxb.bookmark.common.util.*; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.data.redis.core.StringRedisTemplate; -import org.springframework.stereotype.Service; -import org.springframework.web.multipart.MultipartFile; - -import java.io.File; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.util.HashMap; -import java.util.Map; -import java.util.UUID; -import java.util.concurrent.TimeUnit; - -/** - * 类功能简述: - * 类功能详述: - * - * @author fanxb - * @date 2019/7/5 17:39 - */ -@Service -public class UserService { - - private static final String DEFAULT_ICON = "/favicon.ico"; - /** - * 短期jwt失效时间 - */ - private static final long SHORT_EXPIRE_TIME = 2 * 60 * 60 * 1000; - /** - * 长期jwt失效时间 - */ - private static final long LONG_EXPIRE_TIME = 30L * TimeUtil.DAY_MS; - - /** - * 头像文件大小限制 单位:KB - */ - private static final int ICON_SIZE = 200; - - @Autowired - private UserDao userDao; - @Autowired - private StringRedisTemplate redisTemplate; - - /** - * Description: 向目标发送验证码 - * - * @param email 目标 - * @author fanxb - * @date 2019/7/5 17:48 - */ - public void sendAuthCode(String email) { - MailInfo info = new MailInfo(); - info.setSubject("签签世界注册验证码"); - String code = StringUtil.getRandomString(6, 2); - info.setContent("欢迎注册 签签世界 ,本次验证码"); - info.setContent(code + " 是您的验证码,注意验证码有效期为15分钟哦!"); - info.setReceiver(email); - if (Constant.isDev) { - code = "123456"; - } else { - MailUtil.sendTextMail(info); - } - RedisUtil.set(Constant.authCodeKey(email), code, Constant.AUTH_CODE_EXPIRE); - } - - /** - * Description: 用户注册 - * - * @param body 注册表单 - * @author fanxb - * @date 2019/7/6 11:30 - */ - public LoginRes register(RegisterBody body) { - User user = userDao.selectByUsernameOrEmail(body.getUsername(), body.getEmail()); - if (user != null) { - if (user.getUsername().equals(body.getUsername())) { - throw new FormDataException("用户名已经被注册"); - } - if (user.getEmail().equals(body.getEmail())) { - throw new FormDataException("邮箱已经被注册"); - } - } - user = new User(); - user.setUsername(body.getUsername()); - user.setEmail(body.getEmail()); - user.setIcon(DEFAULT_ICON); - user.setPassword(HashUtil.sha1(HashUtil.md5(body.getPassword()))); - user.setCreateTime(System.currentTimeMillis()); - user.setLastLoginTime(System.currentTimeMillis()); - user.setVersion(0); - userDao.addOne(user); - Map data = new HashMap<>(1); - data.put("userId", String.valueOf(user.getUserId())); - String token = JwtUtil.encode(data, Constant.jwtSecret, LONG_EXPIRE_TIME); - return new LoginRes(user, token); - } - - /** - * Description: 登录 - * - * @param body 登录表单 - * @return LoginRes - * @author fanxb - * @date 2019/7/6 16:37 - */ - public LoginRes login(LoginBody body) { - String key = RedisConstant.getUserFailCountKey(body.getStr()); - String count = redisTemplate.opsForValue().get(key); - if (count != null && Integer.parseInt(count) >= 5) { - redisTemplate.expire(key, 30, TimeUnit.MINUTES); - throw new FormDataException("您已连续输错密码5次,请30分钟后再试,或联系管理员处理"); - } - User userInfo = userDao.selectByUsernameOrEmail(body.getStr(), body.getStr()); - if (userInfo == null || !HashUtil.sha1(HashUtil.md5(body.getPassword())).equals(userInfo.getPassword())) { - redisTemplate.opsForValue().set(key, count == null ? "1" : String.valueOf(Integer.parseInt(count) + 1), 30, TimeUnit.MINUTES); - throw new FormDataException("账号密码错误"); - } - redisTemplate.delete(key); - Map data = new HashMap<>(1); - data.put("userId", String.valueOf(userInfo.getUserId())); - String token = JwtUtil.encode(data, Constant.jwtSecret, body.isRememberMe() ? LONG_EXPIRE_TIME : SHORT_EXPIRE_TIME); - LoginRes res = new LoginRes(); - res.setToken(token); - res.setUser(userInfo); - userDao.updateLastLoginTime(System.currentTimeMillis(), userInfo.getUserId()); - return res; - } - - /** - * Description: 重置密码 - * - * @param body 重置密码 由于参数和注册差不多,所以用同一个表单 - * @author fanxb - * @date 2019/7/9 19:59 - */ - public void resetPassword(RegisterBody body) { - User user = userDao.selectByUsernameOrEmail(body.getEmail(), body.getEmail()); - if (user == null) { - throw new FormDataException("用户不存在"); - } - String codeKey = Constant.authCodeKey(body.getEmail()); - String realCode = RedisUtil.get(codeKey, String.class); - if (StringUtil.isEmpty(realCode) || (!realCode.equals(body.getAuthCode()))) { - throw new FormDataException("验证码错误"); - } - RedisUtil.delete(codeKey); - String newPassword = HashUtil.getPassword(body.getPassword()); - userDao.resetPassword(newPassword, body.getEmail()); - } - - /** - * Description: 根据userId获取用户信息 - * - * @param userId userId - * @return com.fanxb.bookmark.common.entity.User - * @author fanxb - * @date 2019/7/30 15:57 - */ - public User getUserInfo(int userId) { - return userDao.selectByUserId(userId); - } - - /** - * 修改用户头像 - * - * @param file file - * @return 访问路径 - */ - public String updateIcon(MultipartFile file) throws Exception { - if (file.getSize() / NumberConstant.K_SIZE > ICON_SIZE) { - throw new FormDataException("文件大小超过限制"); - } - int userId = UserContextHolder.get().getUserId(); - String fileName = file.getOriginalFilename(); - assert fileName != null; - String path = Paths.get(FileConstant.iconPath, userId + "." + System.currentTimeMillis() + fileName.substring(fileName.lastIndexOf("."))).toString(); - Path realPath = Paths.get(Constant.fileSavePath, path); - FileUtil.ensurePathExist(realPath.getParent().toString()); - file.transferTo(realPath); - path = File.separator + path; - userDao.updateUserIcon(userId, path); - return path; - } - - /** - * 功能描述: 密码校验,校验成功返回一个actionId,以执行敏感操作 - * - * @param password password - * @return java.lang.String - * @author fanxb - * @date 2019/11/11 23:41 - */ - public String checkPassword(String password) { - int userId = UserContextHolder.get().getUserId(); - String pass = HashUtil.getPassword(password); - User user = userDao.selectByUserId(userId); - if (!user.getPassword().equals(pass)) { - throw new FormDataException("密码错误,请重试"); - } - String actionId = UUID.randomUUID().toString().replaceAll("-", ""); - String key = RedisConstant.getPasswordCheckKey(userId, actionId); - RedisUtil.set(key, "1", 5 * 60 * 1000); - return actionId; - } -} +package com.fanxb.bookmark.business.user.service; + +import com.fanxb.bookmark.common.util.TimeUtil; + +/** + * 用户接口 + * + * @author fanxb + * @date 2021/3/11 + **/ +public interface UserService { + static final String DEFAULT_ICON = "/favicon.ico"; + /** + * 短期jwt失效时间 + */ + static final long SHORT_EXPIRE_TIME = 2 * 60 * 60 * 1000; + /** + * 长期jwt失效时间 + */ + static final long LONG_EXPIRE_TIME = 30L * TimeUtil.DAY_MS; + + /** + * 头像文件大小限制 单位:KB + */ + static final int ICON_SIZE = 200; + + /*** + * 获取一个可用的用户名 + * @author fanxb + * @return java.lang.String + * @date 2021/3/11 + **/ + String createNewUsername(); + + /** + * 获取当前用户的version + * + * @return int + * @author fanxb + * @date 2021/3/11 + **/ + int getCurrentUserVersion(int userId); +} diff --git a/bookMarkService/business/user/src/main/java/com/fanxb/bookmark/business/user/service/impl/OAuthServiceImpl.java b/bookMarkService/business/user/src/main/java/com/fanxb/bookmark/business/user/service/impl/OAuthServiceImpl.java new file mode 100644 index 0000000..8eaa4a6 --- /dev/null +++ b/bookMarkService/business/user/src/main/java/com/fanxb/bookmark/business/user/service/impl/OAuthServiceImpl.java @@ -0,0 +1,116 @@ +package com.fanxb.bookmark.business.user.service.impl; + +import cn.hutool.core.util.RandomUtil; +import cn.hutool.core.util.StrUtil; +import com.alibaba.fastjson.JSONObject; +import com.fanxb.bookmark.business.user.dao.UserDao; +import com.fanxb.bookmark.business.user.service.OAuthService; +import com.fanxb.bookmark.business.user.service.UserService; +import com.fanxb.bookmark.business.user.vo.OAuthBody; +import com.fanxb.bookmark.common.constant.Constant; +import com.fanxb.bookmark.common.entity.User; +import com.fanxb.bookmark.common.exception.CustomException; +import com.fanxb.bookmark.common.util.HttpUtil; +import com.fanxb.bookmark.common.util.JwtUtil; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import static com.fanxb.bookmark.business.user.service.UserService.LONG_EXPIRE_TIME; +import static com.fanxb.bookmark.business.user.service.UserService.SHORT_EXPIRE_TIME; + +/** + * OAuth交互类 + * + * @author fanxb + * @date 2021/3/10 + **/ +@Service +@Slf4j +public class OAuthServiceImpl implements OAuthService { + + @Value("${OAuth.github.clientId}") + private String githubClientId; + @Value("${OAuth.github.secret}") + private String githubSecret; + private final UserDao userDao; + private final UserService userService; + + @Autowired + public OAuthServiceImpl(UserDao userDao, UserService userService) { + this.userDao = userDao; + this.userService = userService; + } + + @Override + @Transactional(rollbackFor = Exception.class) + public String oAuthCheck(OAuthBody body) { + User current, other = new User(); + if (StrUtil.equals(body.getType(), OAuthBody.GITHUB)) { + Map header = new HashMap<>(2); + header.put("accept", "application/json"); + String url = "https://github.com/login/oauth/access_token?client_id=" + githubClientId + "&client_secret=" + githubSecret + "&code=" + body.getCode(); + JSONObject obj = HttpUtil.get(url, header); + String accessToken = obj.getString("access_token"); + if (StrUtil.isEmpty(accessToken)) { + throw new CustomException("github登陆失败,请稍后重试"); + } + header.put("Authorization", "token " + accessToken); + JSONObject userInfo = HttpUtil.get("https://api.github.com/user", header); + other.setGithubId(userInfo.getLong("id")); + if (other.getGithubId() == null) { + log.error("github返回异常:{}", userInfo.toString()); + throw new CustomException("登陆异常,请稍后重试"); + } + other.setEmail(userInfo.getString("email")); + other.setIcon(userInfo.getString("avatar_url")); + other.setUsername(userInfo.getString("login")); + current = userDao.selectByUserIdOrGithubId(null, other.getGithubId()); + if (current == null) { + current = userDao.selectByUsernameOrEmail(null, other.getEmail()); + } + } else { + throw new CustomException("不支持的登陆方式" + body.getType()); + } + User newest = dealOAuth(current, other); + return JwtUtil.encode(Collections.singletonMap("userId", String.valueOf(newest.getUserId())), Constant.jwtSecret + , body.isRememberMe() ? LONG_EXPIRE_TIME : SHORT_EXPIRE_TIME); + } + + /** + * TODO 方法描述 + * + * @param current 当前是否存在该用户 + * @param other 第三方获取的数据 + * @return User 最新的用户信息 + * @author fanxb + * @date 2021/3/11 + **/ + private User dealOAuth(User current, User other) { + if (current == null) { + //判断用户名是否可用 + if (userDao.usernameExist(other.getUsername())) { + other.setUsername(userService.createNewUsername()); + } + other.setPassword(""); + other.setCreateTime(System.currentTimeMillis()); + other.setLastLoginTime(System.currentTimeMillis()); + other.setVersion(0); + userDao.addOne(other); + return other; + } else { + if (!current.getEmail().equals(other.getEmail()) || !current.getGithubId().equals(other.getGithubId())) { + current.setEmail(other.getEmail()); + current.setGithubId(other.getGithubId()); + userDao.updateEmailAndGithubId(current); + } + return current; + } + } +} diff --git a/bookMarkService/business/user/src/main/java/com/fanxb/bookmark/business/user/service/impl/UserServiceImpl.java b/bookMarkService/business/user/src/main/java/com/fanxb/bookmark/business/user/service/impl/UserServiceImpl.java new file mode 100644 index 0000000..a01c0b0 --- /dev/null +++ b/bookMarkService/business/user/src/main/java/com/fanxb/bookmark/business/user/service/impl/UserServiceImpl.java @@ -0,0 +1,219 @@ +package com.fanxb.bookmark.business.user.service.impl; + +import cn.hutool.core.util.RandomUtil; +import cn.hutool.core.util.StrUtil; +import com.fanxb.bookmark.business.user.constant.FileConstant; +import com.fanxb.bookmark.business.user.dao.UserDao; +import com.fanxb.bookmark.business.user.service.UserService; +import com.fanxb.bookmark.business.user.vo.LoginBody; +import com.fanxb.bookmark.business.user.vo.LoginRes; +import com.fanxb.bookmark.business.user.vo.RegisterBody; +import com.fanxb.bookmark.common.constant.Constant; +import com.fanxb.bookmark.common.constant.NumberConstant; +import com.fanxb.bookmark.common.constant.RedisConstant; +import com.fanxb.bookmark.common.entity.MailInfo; +import com.fanxb.bookmark.common.entity.User; +import com.fanxb.bookmark.common.exception.FormDataException; +import com.fanxb.bookmark.common.util.*; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.stereotype.Service; +import org.springframework.web.multipart.MultipartFile; + +import java.io.File; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.TimeUnit; + +/** + * 类功能简述: + * 类功能详述: + * + * @author fanxb + * @date 2019/7/5 17:39 + */ +@Service +public class UserServiceImpl implements UserService { + + @Autowired + private UserDao userDao; + @Autowired + private StringRedisTemplate redisTemplate; + + /** + * Description: 向目标发送验证码 + * + * @param email 目标 + * @author fanxb + * @date 2019/7/5 17:48 + */ + public void sendAuthCode(String email) { + MailInfo info = new MailInfo(); + info.setSubject("签签世界注册验证码"); + String code = StringUtil.getRandomString(6, 2); + info.setContent("欢迎注册 签签世界 ,本次验证码"); + info.setContent(code + " 是您的验证码,注意验证码有效期为15分钟哦!"); + info.setReceiver(email); + if (Constant.isDev) { + code = "123456"; + } else { + MailUtil.sendTextMail(info); + } + RedisUtil.set(Constant.authCodeKey(email), code, Constant.AUTH_CODE_EXPIRE); + } + + /** + * Description: 用户注册 + * + * @param body 注册表单 + * @author fanxb + * @date 2019/7/6 11:30 + */ + public String register(RegisterBody body) { + User user = userDao.selectByUsernameOrEmail(body.getUsername(), body.getEmail()); + if (user != null) { + if (user.getUsername().equals(body.getUsername())) { + throw new FormDataException("用户名已经被注册"); + } + if (user.getEmail().equals(body.getEmail())) { + throw new FormDataException("邮箱已经被注册"); + } + } + user = new User(); + user.setUsername(body.getUsername()); + user.setEmail(body.getEmail()); + user.setIcon(DEFAULT_ICON); + user.setPassword(HashUtil.sha1(HashUtil.md5(body.getPassword()))); + user.setCreateTime(System.currentTimeMillis()); + user.setLastLoginTime(System.currentTimeMillis()); + user.setVersion(0); + userDao.addOne(user); + Map data = new HashMap<>(1); + data.put("userId", String.valueOf(user.getUserId())); + return JwtUtil.encode(data, Constant.jwtSecret, LONG_EXPIRE_TIME); + } + + /** + * Description: 登录 + * + * @param body 登录表单 + * @return string + * @author fanxb + * @date 2019/7/6 16:37 + */ + public String login(LoginBody body) { + String key = RedisConstant.getUserFailCountKey(body.getStr()); + String count = redisTemplate.opsForValue().get(key); + if (count != null && Integer.parseInt(count) >= 5) { + redisTemplate.expire(key, 30, TimeUnit.MINUTES); + throw new FormDataException("您已连续输错密码5次,请30分钟后再试,或联系管理员处理"); + } + User userInfo = userDao.selectByUsernameOrEmail(body.getStr(), body.getStr()); + if (userInfo == null || StrUtil.isEmpty(userInfo.getPassword()) || !HashUtil.sha1(HashUtil.md5(body.getPassword())).equals(userInfo.getPassword())) { + redisTemplate.opsForValue().set(key, count == null ? "1" : String.valueOf(Integer.parseInt(count) + 1), 30, TimeUnit.MINUTES); + throw new FormDataException("账号密码错误"); + } + redisTemplate.delete(key); + userDao.updateLastLoginTime(System.currentTimeMillis(), userInfo.getUserId()); + return JwtUtil.encode(Collections.singletonMap("userId", String.valueOf(userInfo.getUserId())), Constant.jwtSecret + , body.isRememberMe() ? LONG_EXPIRE_TIME : SHORT_EXPIRE_TIME); + } + + /** + * Description: 重置密码 + * + * @param body 重置密码 由于参数和注册差不多,所以用同一个表单 + * @author fanxb + * @date 2019/7/9 19:59 + */ + public void resetPassword(RegisterBody body) { + User user = userDao.selectByUsernameOrEmail(body.getEmail(), body.getEmail()); + if (user == null) { + throw new FormDataException("用户不存在"); + } + String codeKey = Constant.authCodeKey(body.getEmail()); + String realCode = RedisUtil.get(codeKey, String.class); + if (StringUtil.isEmpty(realCode) || (!realCode.equals(body.getAuthCode()))) { + throw new FormDataException("验证码错误"); + } + RedisUtil.delete(codeKey); + String newPassword = HashUtil.getPassword(body.getPassword()); + userDao.resetPassword(newPassword, body.getEmail()); + } + + /** + * Description: 根据userId获取用户信息 + * + * @param userId userId + * @return com.fanxb.bookmark.common.entity.User + * @author fanxb + * @date 2019/7/30 15:57 + */ + public User getUserInfo(int userId) { + User user = userDao.selectByUserIdOrGithubId(userId, null); + user.setNoPassword(StrUtil.isEmpty(user.getPassword())); + return user; + } + + /** + * 修改用户头像 + * + * @param file file + * @return 访问路径 + */ + public String updateIcon(MultipartFile file) throws Exception { + if (file.getSize() / NumberConstant.K_SIZE > ICON_SIZE) { + throw new FormDataException("文件大小超过限制"); + } + int userId = UserContextHolder.get().getUserId(); + String fileName = file.getOriginalFilename(); + assert fileName != null; + String path = Paths.get(FileConstant.iconPath, userId + "." + System.currentTimeMillis() + fileName.substring(fileName.lastIndexOf("."))).toString(); + Path realPath = Paths.get(Constant.fileSavePath, path); + FileUtil.ensurePathExist(realPath.getParent().toString()); + file.transferTo(realPath); + path = File.separator + path; + userDao.updateUserIcon(userId, path); + return path; + } + + /** + * 功能描述: 密码校验,校验成功返回一个actionId,以执行敏感操作 + * + * @param password password + * @return java.lang.String + * @author fanxb + * @date 2019/11/11 23:41 + */ + public String checkPassword(String password) { + int userId = UserContextHolder.get().getUserId(); + String pass = HashUtil.getPassword(password); + User user = userDao.selectByUserIdOrGithubId(userId, null); + if (!user.getPassword().equals(pass)) { + throw new FormDataException("密码错误,请重试"); + } + String actionId = UUID.randomUUID().toString().replaceAll("-", ""); + String key = RedisConstant.getPasswordCheckKey(userId, actionId); + RedisUtil.set(key, "1", 5 * 60 * 1000); + return actionId; + } + + @Override + public String createNewUsername() { + while (true) { + String name = RandomUtil.randomString(8); + if (!userDao.usernameExist(name)) { + return name; + } + } + } + + @Override + public int getCurrentUserVersion(int userId) { + return userDao.getUserVersion(userId); + } +} diff --git a/bookMarkService/business/user/src/main/java/com/fanxb/bookmark/business/user/vo/OAuthBody.java b/bookMarkService/business/user/src/main/java/com/fanxb/bookmark/business/user/vo/OAuthBody.java new file mode 100644 index 0000000..739ae40 --- /dev/null +++ b/bookMarkService/business/user/src/main/java/com/fanxb/bookmark/business/user/vo/OAuthBody.java @@ -0,0 +1,26 @@ +package com.fanxb.bookmark.business.user.vo; + +import lombok.Data; + +/** + * 第三方登陆入参 + * + * @author fanxb + * @date 2021/3/10 + **/ +@Data +public class OAuthBody { + public static final String GITHUB = "github"; + /** + * 类别 + */ + private String type; + /** + * 识别码 + */ + private String code; + /** + * 是否保持登陆 + */ + private boolean rememberMe; +} diff --git a/bookMarkService/business/user/src/main/resources/mapper/user-userMapper.xml b/bookMarkService/business/user/src/main/resources/mapper/user-userMapper.xml index 18f5ff0..3e69bae 100644 --- a/bookMarkService/business/user/src/main/resources/mapper/user-userMapper.xml +++ b/bookMarkService/business/user/src/main/resources/mapper/user-userMapper.xml @@ -4,23 +4,16 @@ - insert into user (username, email, icon, password, createTime, lastLoginTime,version) - value - (#{username}, #{email}, #{icon}, #{password}, #{createTime}, #{lastLoginTime},#{version}) + insert into user (username, email, icon, password, createTime, lastLoginTime, version) + value + (#{username}, #{email}, #{icon}, #{password}, #{createTime}, #{lastLoginTime}, #{version}) @@ -36,10 +29,11 @@ where email = #{email} - select * from user where userId = #{userId} + or githubId = #{githubId} diff --git a/bookMarkService/common/src/main/java/com/fanxb/bookmark/common/entity/User.java b/bookMarkService/common/src/main/java/com/fanxb/bookmark/common/entity/User.java index ba63701..a696212 100644 --- a/bookMarkService/common/src/main/java/com/fanxb/bookmark/common/entity/User.java +++ b/bookMarkService/common/src/main/java/com/fanxb/bookmark/common/entity/User.java @@ -19,10 +19,18 @@ import java.util.Map; public class User { private int userId; + /** + * 第三方github登陆id,-1说明非github登陆 + */ + private Long githubId; private String username; private String email; private String newEmail; private String icon; + /** + * 是否未设置密码 + */ + private Boolean noPassword; @JSONField(serialize = false) private String password; private long createTime; @@ -34,4 +42,5 @@ public class User { * 书签同步版本 */ private int version; + } diff --git a/bookMarkService/common/src/main/java/com/fanxb/bookmark/common/util/HttpUtil.java b/bookMarkService/common/src/main/java/com/fanxb/bookmark/common/util/HttpUtil.java index 1e1627a..2e4a460 100644 --- a/bookMarkService/common/src/main/java/com/fanxb/bookmark/common/util/HttpUtil.java +++ b/bookMarkService/common/src/main/java/com/fanxb/bookmark/common/util/HttpUtil.java @@ -1,11 +1,17 @@ package com.fanxb.bookmark.common.util; +import cn.hutool.core.util.StrUtil; import com.alibaba.fastjson.JSONArray; import com.alibaba.fastjson.JSONObject; import com.fanxb.bookmark.common.exception.CustomException; import okhttp3.*; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; +import javax.annotation.PostConstruct; import javax.servlet.http.HttpServletRequest; +import java.net.InetSocketAddress; +import java.net.Proxy; import java.util.Map; import java.util.Set; import java.util.concurrent.TimeUnit; @@ -17,13 +23,30 @@ import java.util.concurrent.TimeUnit; * @author fanxb * @date 2019/4/4 15:53 */ +@Component public class HttpUtil { + @Value("${proxy.ip}") + private String proxyIp; + @Value("${proxy.port}") + private int proxyPort; + private static final int IP_LENGTH = 15; - public static final OkHttpClient CLIENT = new OkHttpClient.Builder().connectTimeout(3, TimeUnit.SECONDS) - .readTimeout(300, TimeUnit.SECONDS).build(); + private static OkHttpClient CLIENT; public static final MediaType JSON = MediaType.get("application/json; charset=utf-8"); + @PostConstruct + public void init() { + + OkHttpClient.Builder builder = new OkHttpClient.Builder(); + if (StrUtil.isNotBlank(proxyIp)) { + builder.proxy(new Proxy(Proxy.Type.HTTP, new InetSocketAddress(proxyIp, proxyPort))); + } + CLIENT = builder.connectTimeout(10, TimeUnit.SECONDS) + .readTimeout(60, TimeUnit.SECONDS) + .build(); + } + /** * 功能描述: get * @@ -142,6 +165,8 @@ public class HttpUtil { String str = res.body().string(); if (typeClass.getCanonicalName().equals(JSONObject.class.getCanonicalName())) { return (T) JSONObject.parseObject(str); + } else if (typeClass.getCanonicalName().equals(String.class.getCanonicalName())) { + return (T) str; } else { return (T) JSONArray.parseArray(str); } diff --git a/bookMarkService/common/src/main/java/com/fanxb/bookmark/common/util/JwtUtil.java b/bookMarkService/common/src/main/java/com/fanxb/bookmark/common/util/JwtUtil.java index eff0a75..7ae9199 100644 --- a/bookMarkService/common/src/main/java/com/fanxb/bookmark/common/util/JwtUtil.java +++ b/bookMarkService/common/src/main/java/com/fanxb/bookmark/common/util/JwtUtil.java @@ -43,7 +43,7 @@ public class JwtUtil { * * @param token token * @param secret secret - * @return java.util.Map + * @return java.util.Map * @author fanxb * @date 2019/3/4 18:14 */ diff --git a/bookMarkService/web/src/main/resources/application.yml b/bookMarkService/web/src/main/resources/application.yml index 364d37d..2c78d8e 100644 --- a/bookMarkService/web/src/main/resources/application.yml +++ b/bookMarkService/web/src/main/resources/application.yml @@ -76,3 +76,18 @@ fileSavePath: ./ # 服务部署地址 serviceAddress: http://localhost + +# 第三方登陆相关 +OAuth: + github: + # 客户端id + clientId: + # 客户端密钥 + secret: + +# 网络代理(有配置就用代理,未配置不使用代理) +proxy: + ip: + port: + + diff --git a/bookMarkService/web/src/main/resources/db/migration/V15__新增免验证url.sql b/bookMarkService/web/src/main/resources/db/migration/V15__新增免验证url.sql new file mode 100644 index 0000000..09e4062 --- /dev/null +++ b/bookMarkService/web/src/main/resources/db/migration/V15__新增免验证url.sql @@ -0,0 +1 @@ +INSERT INTO `bookmark`.`url`(`method`, `url`, `type`) VALUES ('POST', '/user/oAuthLogin', 0); \ No newline at end of file diff --git a/bookMarkService/web/src/main/resources/db/migration/V16__user表改结构.sql b/bookMarkService/web/src/main/resources/db/migration/V16__user表改结构.sql new file mode 100644 index 0000000..657dffc --- /dev/null +++ b/bookMarkService/web/src/main/resources/db/migration/V16__user表改结构.sql @@ -0,0 +1,14 @@ +alter table user + add githubId bigint default -1 not null comment '-1说明未使用github登陆' after userId; + +create unique index email_index + on user (email); + +create index githubIdIndex + on user (githubId); + +create index new_email_index + on user (newEmail); + +create unique index username_index + on user (username); \ No newline at end of file diff --git a/bookmark_front/.env.development b/bookmark_front/.env.development new file mode 100644 index 0000000..00b5fbd --- /dev/null +++ b/bookmark_front/.env.development @@ -0,0 +1 @@ +VUE_APP_GITHUB_CLIENT_ID= \ No newline at end of file diff --git a/bookmark_front/.env.production b/bookmark_front/.env.production new file mode 100644 index 0000000..00b5fbd --- /dev/null +++ b/bookmark_front/.env.production @@ -0,0 +1 @@ +VUE_APP_GITHUB_CLIENT_ID= \ No newline at end of file diff --git a/bookmark_front/src/App.vue b/bookmark_front/src/App.vue index 21c04ab..ddbbc49 100644 --- a/bookmark_front/src/App.vue +++ b/bookmark_front/src/App.vue @@ -6,7 +6,7 @@ @@ -25,5 +25,6 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; color: #2c3e50; + font-size: 0.16rem; } diff --git a/bookmark_front/src/layout/main/Top.vue b/bookmark_front/src/layout/main/Top.vue index bb69da3..5bde2d6 100644 --- a/bookmark_front/src/layout/main/Top.vue +++ b/bookmark_front/src/layout/main/Top.vue @@ -1,5 +1,5 @@ @@ -34,7 +35,7 @@ export default { width: 2rem; } .main-body { - width: 5rem; + min-width: 5rem; min-height: 3.5rem; background-color: @publicBgColor; border-radius: 5px; diff --git a/bookmark_front/src/views/public/pages/Login.vue b/bookmark_front/src/views/public/pages/Login.vue index c49cc2e..0c91a0c 100644 --- a/bookmark_front/src/views/public/pages/Login.vue +++ b/bookmark_front/src/views/public/pages/Login.vue @@ -1,7 +1,7 @@ @@ -34,52 +38,109 @@ import Header from "@/components/public/Switch.vue"; import httpUtil from "../../../util/HttpUtil.js"; import { mapMutations } from "vuex"; +import HttpUtil from "../../../util/HttpUtil.js"; export default { name: "Login", components: { - Header + Header, }, data() { return { form: { str: "", password: "", - rememberMe: false + rememberMe: false, }, rules: { str: [ { required: true, message: "请输入用户名", trigger: "blur" }, - { min: 1, max: 50, message: "最短1,最长50", trigger: "change" } + { min: 1, max: 50, message: "最短1,最长50", trigger: "change" }, ], password: [ { required: true, message: "请输入密码", trigger: "blur" }, - { pattern: "^\\w{6,18}$", message: "密码为6-18位数字,字母,下划线组合", trigger: "change" } - ] - } + { pattern: "^\\w{6,18}$", message: "密码为6-18位数字,字母,下划线组合", trigger: "change" }, + ], + }, + loading: false, //是否加载中 + oauthLogining: false, //true:正在进行oauth后台操作 + page: null, //oauth打开的页面实例 }; }, + async created() { + let _this = this; + window.addEventListener("storage", this.storageDeal.bind(this)); + }, + destroyed() { + window.removeEventListener("storage", this.storageDeal); + }, methods: { ...mapMutations("globalConfig", ["setUserInfo", "setToken"]), submit() { - this.$refs.loginForm.validate(async status => { + this.$refs.loginForm.validate(async (status) => { if (status) { - let res = await httpUtil.post("/user/login", null, this.form); - this.setUserInfo(res.user); - this.$store.commit("globalConfig/setToken", res.token); - this.$router.replace("/"); + try { + this.loading = true; + let token = await httpUtil.post("/user/login", null, this.form); + this.$store.dispatch("globalConfig/setToken", token); + this.$router.replace("/"); + } finally { + this.loading = false; + } } }); - } - } + }, + toGithub() { + let redirect = location.origin + "/public/oauth/github"; + let clientId = process.env.VUE_APP_GITHUB_CLIENT_ID; + this.page = window.open(`https://github.com/login/oauth/authorize?client_id=${clientId}&redirect_uri=${redirect}`); + }, + async storageDeal(e) { + console.log(e); + if (e.key === "oauthMessage") { + if (this.page != null) { + this.page.close(); + } + localStorage.removeItem("oauthMessage"); + await this.oauthLogin(e.newValue); + } + }, + async oauthLogin(form) { + if (this.loading) { + console.error("正在请求中", form); + return; + } + form = JSON.parse(form); + if (!form.code) { + this.$message.error("您已拒绝,无法继续登陆"); + return; + } + try { + this.loading = true; + form.rememberMe = this.form.rememberMe; + let token = await HttpUtil.post("/user/oAuthLogin", null, form); + this.$store.dispatch("globalConfig/setToken", token); + this.$router.replace("/"); + } finally { + this.loading = false; + } + }, + }, }; diff --git a/bookmark_front/src/views/public/pages/Register.vue b/bookmark_front/src/views/public/pages/Register.vue index 8c512e8..e39bb31 100644 --- a/bookmark_front/src/views/public/pages/Register.vue +++ b/bookmark_front/src/views/public/pages/Register.vue @@ -40,7 +40,7 @@ import httpUtil from "../../../util/HttpUtil.js"; export default { name: "Login", components: { - Header + Header, }, data() { let repeatPass = (rule, value, cb) => { @@ -57,41 +57,40 @@ export default { username: "", email: "", password: "", - repeatPass: "" + repeatPass: "", }, rules: { username: [ { required: true, message: "请输入用户名", trigger: "blur" }, - { min: 1, max: 50, message: "最短1,最长50", trigger: "blur" } + { min: 1, max: 50, message: "最短1,最长50", trigger: "blur" }, ], email: [ { pattern: /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/, message: "请输入正确的邮箱", - trigger: "change" - } + trigger: "change", + }, ], password: [ { required: true, message: "请输入密码", trigger: "blur" }, - { pattern: "^\\w{6,18}$", message: "密码为6-18位数字,字母,下划线组合", trigger: "change" } + { pattern: "^\\w{6,18}$", message: "密码为6-18位数字,字母,下划线组合", trigger: "change" }, ], - repeatPass: [{ validator: repeatPass, trigger: "change" }] - } + repeatPass: [{ validator: repeatPass, trigger: "change" }], + }, }; }, methods: { submit() { let _this = this; - this.$refs.registerForm.validate(async status => { + this.$refs.registerForm.validate(async (status) => { if (status) { let res = await httpUtil.put("/user", null, _this.form); - this.$store.commit("globalConfig/setUserInfo", res.user); - this.$store.commit("globalConfig/setToken", res.token); + this.$store.dispatch("globalConfig/setToken", res); this.$router.replace("/"); } }); - } - } + }, + }, }; diff --git a/bookmark_front/src/views/public/pages/oauth/Github.vue b/bookmark_front/src/views/public/pages/oauth/Github.vue new file mode 100644 index 0000000..fa12453 --- /dev/null +++ b/bookmark_front/src/views/public/pages/oauth/Github.vue @@ -0,0 +1,27 @@ + + + + + \ No newline at end of file