feat:github登陆接入

This commit is contained in:
fanxb 2021-03-11 16:53:36 +08:00
parent cc71076b45
commit 1196926ac9
29 changed files with 740 additions and 342 deletions

View File

@ -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()));
}
}

View File

@ -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
@ -103,10 +104,10 @@ public interface UserDao {
* 更新用户新邮箱
*
* @param userId userId
* @param newPassword 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);
}

View File

@ -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");
}

View File

@ -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);
}

View File

@ -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;
import com.fanxb.bookmark.common.util.TimeUtil;
/**
* 类功能简述
* 类功能详述
* 用户接口
*
* @author fanxb
* @date 2019/7/5 17:39
*/
@Service
public class UserService {
private static final String DEFAULT_ICON = "/favicon.ico";
* @date 2021/3/11
**/
public interface UserService {
static final String DEFAULT_ICON = "/favicon.ico";
/**
* 短期jwt失效时间
*/
private static final long SHORT_EXPIRE_TIME = 2 * 60 * 60 * 1000;
static final long SHORT_EXPIRE_TIME = 2 * 60 * 60 * 1000;
/**
* 长期jwt失效时间
*/
private static final long LONG_EXPIRE_TIME = 30L * TimeUtil.DAY_MS;
static final long LONG_EXPIRE_TIME = 30L * TimeUtil.DAY_MS;
/**
* 头像文件大小限制 单位KB
*/
private static final int ICON_SIZE = 200;
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<String, String> 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<String, String> 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
* @date 2021/3/11
**/
String createNewUsername();
/**
* 获取当前用户的version
*
* @return int
* @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;
}
* @date 2021/3/11
**/
int getCurrentUserVersion(int userId);
}

View File

@ -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<String, String> 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;
}
}
}

View File

@ -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<String, String> 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);
}
}

View File

@ -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;
}

View File

@ -10,17 +10,10 @@
</insert>
<select id="selectByUsernameOrEmail" resultType="com.fanxb.bookmark.common.entity.User">
select
userId,
username,
email,
icon,
password,
createTime,
lastLoginTime,
version
select *
from user
where username = #{name} or email = #{email}
where username = #{name}
or email = #{email}
limit 1
</select>
@ -36,10 +29,11 @@
where email = #{email}
</update>
<select id="selectByUserId" resultType="com.fanxb.bookmark.common.entity.User">
<select id="selectByUserIdOrGithubId" resultType="com.fanxb.bookmark.common.entity.User">
select *
from user
where userId = #{userId}
or githubId = #{githubId}
</select>

View File

@ -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;
}

View File

@ -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);
}

View File

@ -76,3 +76,18 @@ fileSavePath: ./
# 服务部署地址
serviceAddress: http://localhost
# 第三方登陆相关
OAuth:
github:
# 客户端id
clientId:
# 客户端密钥
secret:
# 网络代理(有配置就用代理,未配置不使用代理)
proxy:
ip:
port:

View File

@ -0,0 +1 @@
INSERT INTO `bookmark`.`url`(`method`, `url`, `type`) VALUES ('POST', '/user/oAuthLogin', 0);

View File

@ -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);

View File

@ -0,0 +1 @@
VUE_APP_GITHUB_CLIENT_ID=

View File

@ -0,0 +1 @@
VUE_APP_GITHUB_CLIENT_ID=

View File

@ -6,7 +6,7 @@
<script>
export default {
name: "App"
name: "App",
};
</script>
@ -25,5 +25,6 @@ body {
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
color: #2c3e50;
font-size: 0.16rem;
}
</style>

View File

@ -1,5 +1,5 @@
<template>
<div class="main">
<div class="main" v-if="userInfo">
<a class="ico" href="/"><img src="/static/img/bookmarkLogo.png" /></a>
<a-dropdown>
<div class="user">
@ -31,7 +31,7 @@ export default {
await this.$store.dispatch("globalConfig/clear");
this.$router.replace("/public/login");
} else if (key === "personSpace") {
this.$router.push("personSpace/userInfo");
this.$router.push("/personSpace/userInfo");
}
},
},

View File

@ -8,6 +8,7 @@ import Public from "../views/public/Public.vue";
import Login from "../views/public/pages/Login.vue";
import Register from "../views/public/pages/Register.vue";
import ResetPassword from "../views/public/pages/ResetPassword.vue";
import GithubOauth from "../views/public/pages/oauth/Github.vue";
Vue.use(VueRouter);
@ -47,12 +48,18 @@ const routes = [
path: "resetPassword",
name: "ResetPassword",
component: ResetPassword
},
{
path: "oauth/github",
name: "GithubRedirect",
component: GithubOauth
}
]
}
];
const router = new VueRouter({
mode: "history",
routes
});

View File

@ -1,5 +1,8 @@
import localforage from "localforage";
import HttpUtil from "../../util/HttpUtil";
const USER_INFO = "userInfo";
const TOKEN = "token";
/**
* 存储全局配置
*/
@ -7,11 +10,11 @@ const state = {
/**
* 用户信息
*/
userInfo: {},
[USER_INFO]: {},
/**
* token
*/
token: null,
[TOKEN]: null,
/**
* 是否已经初始化完成,避免多次重复初始化
*/
@ -30,36 +33,43 @@ const actions = {
if (context.state.isInit) {
return;
}
context.commit("setUserInfo", await localforage.getItem("userInfo"));
const token = await localforage.getItem("token");
window.token = token;
context.commit("setToken", token);
const token = await localforage.getItem(TOKEN);
await context.dispatch("setToken", token);
let userInfo = await localforage.getItem(USER_INFO);
if (userInfo === null || userInfo === "") {
await context.dispatch("refreshUserInfo");
} else {
context.commit(USER_INFO, userInfo);
}
context.commit("isInit", true);
context.commit("isPhone", /Android|webOS|iPhone|iPod|BlackBerry/i.test(navigator.userAgent));
},
async refreshUserInfo({ commit }) {
let userInfo = await HttpUtil.get("/user/currentUserInfo");
await localforage.setItem("userInfo", userInfo);
commit("setUserInfo", userInfo);
await localforage.setItem(USER_INFO, userInfo);
commit(USER_INFO, userInfo);
},
async setToken({ commit }, token) {
await localforage.setItem(TOKEN, token);
commit(TOKEN, token);
},
//登出清除数据
async clear(context) {
await localforage.removeItem("userInfo");
await localforage.removeItem("token");
delete window.token;
context.commit("setUserInfo", {});
context.commit("setToken", null);
context.commit(USER_INFO, null);
context.commit(TOKEN, null);
context.commit("isInit", false);
}
};
const mutations = {
setUserInfo(state, userInfo) {
localforage.setItem("userInfo", userInfo);
userInfo(state, userInfo) {
state.userInfo = userInfo;
},
setToken(state, token) {
localforage.setItem("token", token);
window.token = token;
token(state, token) {
state.token = token;
},
isInit(state, isInit) {

View File

@ -39,14 +39,14 @@ const getters = {
const actions = {
//从缓存初始化数据
async init(context) {
if (context.state.isInit) {
if (context.state.isInit || context.state.isIniting) {
return;
}
context.commit("isIniting", true);
let userInfo = await httpUtil.get("/user/currentUserInfo");
let realVersion = await httpUtil.get("/user/version");
let data = await localforage.getItem(TOTAL_TREE_DATA);
let version = await localforage.getItem(VERSION);
if (!data || userInfo.version > version) {
if (!data || realVersion > version) {
await context.dispatch("refresh");
} else {
context.commit(TOTAL_TREE_DATA, data);
@ -85,8 +85,8 @@ const actions = {
item1.scopedSlots = { title: "nodeTitle" };
})
);
let userInfo = await httpUtil.get("/user/currentUserInfo");
await context.dispatch("updateVersion", userInfo.version);
let version = await httpUtil.get("/user/version");
await context.dispatch("updateVersion", version);
context.commit(TOTAL_TREE_DATA, treeData);
await localforage.setItem(TOTAL_TREE_DATA, treeData);
},
@ -243,7 +243,7 @@ const actions = {
};
const mutations = {
totalTreeData(state, totalTreeData) {
[TOTAL_TREE_DATA]: (state, totalTreeData) => {
state.totalTreeData = totalTreeData;
},
isInit(state, isInit) {
@ -253,7 +253,7 @@ const mutations = {
state.isIniting = isIniting;
},
version(state, version) {
[VERSION]: (state, version) => {
state[VERSION] = version;
}
};

View File

@ -1,5 +1,5 @@
import * as http from "axios";
import { getToken } from "./UserUtil";
import vuex from "../store/index.js";
import router from "../router/index";
/**
@ -19,7 +19,7 @@ async function request(url, method, params, body, isForm, redirect) {
method,
params,
headers: {
"jwt-token": await getToken()
"jwt-token": vuex.state.globalConfig.token
}
};
if (isForm) {

View File

@ -1,26 +1,4 @@
import localStore from "localforage";
// import HttpUtil from './HttpUtil.js';
const TOKEN = "token";
// consts USER_INFO = "userInfo";
/**
* 获取用户token
*/
export async function getToken() {
if (!window.token) {
window.token = await localStore.getItem(TOKEN);
}
return window.token;
}
/**
* 清除用户token
*/
export async function clearToken() {
delete window.token;
await localStore.removeItem(TOKEN);
}
/**
* 本地获取用户信息
*/

View File

@ -52,10 +52,9 @@ export default {
return;
}
this.count = 0;
let userInfo = await httpUtil.get("/user/currentUserInfo");
this.$store.commit("globalConfig/setUserInfo", userInfo);
let version = await httpUtil.get("/user/version");
const _this = this;
if (this.$store.state.treeData.version < userInfo.version) {
if (this.$store.state.treeData.version < version) {
this.isOpen = true;
this.$confirm({
title: "书签数据有更新,是否立即刷新?",

View File

@ -3,6 +3,7 @@
<img class="ico" src="/static/img/bookmarkLogo.png" />
<div class="main-body">
<router-view />
</div>
</div>
</template>
@ -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;

View File

@ -1,7 +1,7 @@
<template>
<div>
<Header current="login" />
<div class="form">
<a-spin class="form" :delay="100" :spinning="loading">
<a-form-model ref="loginForm" :model="form" :rules="rules">
<a-form-model-item prop="str" ref="str">
<a-input v-model="form.str" placeholder="邮箱/用户名">
@ -13,20 +13,24 @@
<a-icon slot="prefix" type="password" style="color:rgba(0,0,0,.25)" />
</a-input>
</a-form-model-item>
<a-form-model-item prop="password">
<div class="reset">
<a-checkbox v-model="form.rememberMe">记住我</a-checkbox>
<router-link to="resetPassword" replace>重置密码</router-link>
</div>
</a-form-model-item>
<a-form-model-item>
<div class="btns">
<a-button type="primary" block @click="submit">登录</a-button>
</div>
<div class="thirdPart">
<span>第三方登陆</span>
<a-tooltip title="github登陆" class="oneIcon" placement="bottom">
<a-icon type="github" @click="toGithub" style="font-size:1.4em" />
</a-tooltip>
</div>
</a-form-model-item>
</a-form-model>
</div>
</a-spin>
</div>
</template>
@ -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);
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;
}
},
},
};
</script>
<style lang="less" scoped>
.form {
margin: 0.3rem;
margin-bottom: 0.1rem;
.reset {
display: flex;
justify-content: space-between;
}
}
.thirdPart {
display: flex;
justify-content: space-between;
font-size: 1.2em;
align-items: center;
}
</style>

View File

@ -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("/");
}
});
}
}
},
},
};
</script>

View File

@ -0,0 +1,27 @@
<template>
<div class="github">处理中请稍候</div>
</template>
<script>
export default {
name: "GithubRedirect",
data() {
return {};
},
mounted() {
let body = {
type: "github",
code: this.$route.query.code,
};
localStorage.setItem("oauthMessage", JSON.stringify(body));
},
};
</script>
<style lang="less" scoped>
.github {
width: 100%;
height: 100%;
text-align: center;
}
</style>