努力好了,时间会给你答案。--------magic_guo
在微服务项目中,基于session的登录系统逐渐的被摒弃,随之代替的是单点登录;session登录将session保存在服务器端,但是微服务系统中有很多服务模块,不能保证每个模块都同步用户的session,而且同步了session也使得系统的开销很大;
在单点登录中,令牌token保存在客户端,用户登录时携带token,并统一由路由网关做验证(通过JWT实现),然后再转发到其他模块,免去了系统保存session的步骤;
总的来说,登录模块包括三个部分:登录、注册、忘记密码。其中还夹杂着一些对邮件或手机短信服务的调用,对redis、MQ服务的调用,但是只要把思路捋顺了,问题也就迎刃而解;
准备条件:
mq系统的搭建、reids服务的搭建、邮件服务(上一篇已经叙述)、JWT验证、密码加密工具类;
redis配置文件:
spring:
redis:
host: xxxxxxxxxxxxx
JWT:
依赖:
<!--JWT关于token的工具类依赖-->
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.4.0</version>
</dependency>
代码:
@Slf4j
public class JWTUtils {
/**
* 生成token,将username和userId设置进负载部分
* @param payload
* @param time
* @return
*/
public static String createToken(Map<String, String> payload, Integer time) {
// 创建一个JWTBuilder
JWTCreator.Builder builder = JWT.create();
// 给token设置一个过期时间
Calendar calendar = Calendar.getInstance();
if (time == null) {
// 默认设置超时时间为30分钟
calendar.add(Calendar.MINUTE, 30);
} else {
// 初始化时设置自定义的超时时间
calendar.add(Calendar.MINUTE, time);
}
// 设置负载(用户的铭感数据不可放入负载中,但是用户名和用户id可以放进去) 【第二部分】
Set<Map.Entry<String, String>> entries = payload.entrySet();
for (Map.Entry<String, String> entry : entries) {
builder.withClaim(entry.getKey(), entry.getValue());
}
// 生成签名
String token = builder
// 设置过期时间
.withExpiresAt(calendar.getTime())
.sign(Algorithm.HMAC256(ShopConstants.JWT_SIGN));
return token;
}
/**
* 校验token是否正确
* @param token
* @return
* @throws Exception
*/
public static DecodedJWT verify(String token) throws Exception {
JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256(ShopConstants.JWT_SIGN)).build();
DecodedJWT verify = jwtVerifier.verify(token);
return verify;
}
}
密码加密:
依赖:
<!--密码加密的工具类依赖-->
<dependency>
<groupId>org.mindrot</groupId>
<artifactId>jbcrypt</artifactId>
<version>0.4</version>
</dependency>
代码:
public class PasswordUtils {
/**
* 密码加密
* @param password 密码
* @return 返回加密后的密码
*/
public static String encode(String password) {
return BCrypt.hashpw(password,BCrypt.gensalt());
}
public static Boolean checkpw(String password, String dbpassword) {
return BCrypt.checkpw(password, dbpassword);
}
}
MQ配置文件:
spring:
rabbitmq:
host: xxxxxxxxxxxx
port: 5672
username: admin
password: admin
virtual-host: /
注册MQ配置类:
@Configuration
public class RabbitMqConfig {
// 创建一个交换机
@Bean
public TopicExchange emailExchange() {
return new TopicExchange(ShopConstants.EMAIL_EXCHANGE, true, false);
}
// 创建一个队列
@Bean
public Queue emailQueue() {
return new Queue(ShopConstants.EMAIL_QUEUE, true, false, false);
}
// 将队列和交换机绑定
@Bean
public Binding bindingEmailQueueToEmailExchange() {
return BindingBuilder.bind(emailQueue()).to(emailExchange()).with("email.*");
}
}
邮件服务MQ的监听器:
@Configuration
@Slf4j
public class EmailQueueListener {
// 创建一个线程池,用来优化消费者处理消息的能力
private ExecutorService executorService = Executors.newFixedThreadPool(5);
@Autowired
private IEmailService emailService;
@RabbitListener(queues = ShopConstants.EMAIL_QUEUE)
public void sendEmail(Email email, Channel channel, Message message) throws MessagingException {
System.out.println("邮件监听已启动!..................");
log.debug("{}", email);
executorService.submit(new Runnable() {
@Override
public void run() {
try {
// 1.调用email服务发送邮件
emailService.sendEmail(email);
// 2.手动ACK
long deliveryTag = message.getMessageProperties().getDeliveryTag(); // 消息的唯一标识
channel.basicAck(deliveryTag, false);
} catch (IOException | MessagingException e) {
e.printStackTrace();
}
}
});
}
}
注册流程图:
忘记密码流程图:
登录流程图:
sso配置文件:
server:
port: 8009
spring:
application:
name: shop-sso
spring:
cloud:
config:
uri: http://localhost:9999
name: application
profile: shop-sso,eureka-client,redis,log,mq,shop-feign
整个Controller层:
@RequestMapping("/sso")
@RestController
@Slf4j
public class SsoUserController {
@Autowired
private IUserService userService;
@Autowired
private RabbitTemplate rabbitTemplate;
@Autowired
private RedisTemplate redisTemplate;
@Autowired
private StringRedisTemplate stringRedisTemplate;
@PostConstruct
public void init(){
redisTemplate.setKeySerializer(new StringRedisSerializer());
}
/**
* 根据用户名获取用户,查询用户名是否存在;
* 功能:校验用户名是否被注册
* @param username 用户名
* @return 返回结果信息
*/
@RequestMapping("/verifyUsername")
public ResultEntity verifyUsername(String username) {
// 查询用户是否存在
User resultEntity = userService.getUserByUsername(username);
if (resultEntity != null) {
return ResultEntity.success("该用户名可使用!");
} else {
return ResultEntity.success("该用户名已被占用!");
}
}
/**
* 根据邮箱获取用户,查询邮箱是否存在;
* 功能:校验邮箱是否被注册
* @param emailStr 邮箱地址
* @return 返回结果信息
*/
@RequestMapping("/verifyEmail")
public ResultEntity verifyEmail(String emailStr) {
User entity = userService.getUserByEmail(emailStr);
if (entity != null) {
return ResultEntity.success("该邮箱可使用!");
} else {
return ResultEntity.success("该邮箱已被占用!");
}
}
/**
* 注册时发送验证码的邮件服务
* @param emailStr 邮件地址
* @return
*/
@RequestMapping("/registerMailSend")
public ResultEntity registerMailSend(String emailStr) {
// 随机生成一个验证码
String code = RandomStringUtils.random(6, false, true);
// 把验证码保存到redis中, 把邮箱和验证码绑定, 并设置有效时间为60秒
stringRedisTemplate.opsForValue().set(ShopConstants.SSO_REGISTER_KEY + emailStr, code, 60, TimeUnit.SECONDS);
// 创建一个邮箱对象
Email email = new Email();
email.setTitle("电商新用户注册");
email.setContent("您得验证码为:" + code);
email.setToUser(emailStr);
// 调用邮件服务,异步形式
rabbitTemplate.convertAndSend(ShopConstants.EMAIL_EXCHANGE, ShopConstants.EMAIL_ROUTING_KEY, email);
return ResultEntity.success("ok");
}
/**
* 注册用户接口
* @param user User对象
* @param code 邮件服务发送的验证码
* @return
*/
@RequestMapping("/registerUser")
public ResultEntity registerUser(User user, String code) {
// 验证用户输入的验证码是否正确
// 从redis中取出验证码
String redisCode = stringRedisTemplate.opsForValue().get(ShopConstants.SSO_REGISTER_KEY + user.getEmail());
// 判断是否为空
if (redisCode == null) {
throw new ShopException("验证码已失效", 10001);
}
// 比对验证码是否一样
if (!redisCode.equals(code)) {
throw new ShopException("验证码有误!", 10002);
}
// 校验用户是否被注册
User resultEntity = userService.getUserByUsername(user.getUsername());
if (resultEntity != null) {
throw new ShopException("该用户名已被注册!", 10003);
}
// 校验邮箱是否被注册
User userByEmail = userService.getUserByEmail(user.getEmail());
if (userByEmail != null) {
throw new ShopException("该邮箱已被注册!", 10004);
}
// 将新用户添加到数据库
// 密码加密
user.setPassword(PasswordUtils.encode(user.getPassword()));
userService.addUser(user);
return ResultEntity.success("注册成功");
}
/**
* 修改密码邮件服务
* @param username 用户名
* @return
*/
@RequestMapping("/forgetPasswordEmailSend")
public ResultEntity forgetPasswordEmailSend(String username) {
// 查询用户名是否存在
User userByUsername = userService.getUserByUsername(username);
if (userByUsername == null) {
return ResultEntity.error("该用户没有被注册");
}
// 生成随机验证码
String code = RandomStringUtils.random(6, false, true);
// 获取此用户的邮箱
String emailStr = userByUsername.getEmail();
// 将验证码保存到redis中,验证码和邮箱绑定, 并设置有效时间为60秒
stringRedisTemplate.opsForValue().set(ShopConstants.SSO_UPDATEPASSWORD_KEY + emailStr, code, 60, TimeUnit.SECONDS);
// 创建一个邮箱对象
Email email = new Email();
email.setTitle("用户" + userByUsername.getUsername() + "密码修改");
email.setContent("您得验证码为:" + code);
email.setToUser(emailStr);
// 发送邮件
rabbitTemplate.convertAndSend(ShopConstants.EMAIL_EXCHANGE, ShopConstants.EMAIL_ROUTING_KEY, email);
return ResultEntity.success("ok");
}
/**
* 修改密码接口
* @param user User对象
* @param code 验证码
* @return
*/
@RequestMapping("/updatePassword")
public ResultEntity updatePassword(User user, String code) {
// 校验验证码是否有效
// 从redis中取出验证码
String redisCode = stringRedisTemplate.opsForValue().get(ShopConstants.SSO_UPDATEPASSWORD_KEY + user.getEmail());
// 判断是否为空
if (redisCode == null) {
throw new ShopException("验证码已失效", 10001);
}
// 比对验证码是否一样
if (!redisCode.equals(code)) {
throw new ShopException("验证码有误!", 10002);
}
// 更新用户的密码
// 将密码加密
user.setPassword(PasswordUtils.encode(user.getPassword()));
userService.updateUser(user);
return ResultEntity.success("修改密码成功");
}
/**
* 在修改密码是使用,校验用户新密码是否与旧密码一致, 异步请求
* @param username,password 用户名和密码
* @return 返回ResultEntity
*/
@RequestMapping("/checkNewPasswordWithOld")
public ResultEntity checkNewPasswordWithOld(String username, String password) {
// 从数据库中取出密码
User userByUsername = userService.getUserByUsername(username);
String dbPassword = userByUsername.getPassword();
// 比对密码
Boolean checkpw = PasswordUtils.checkpw(password, dbPassword);
return ResultEntity.success(checkpw);
}
/**
* 登录接口
* @param username 用户名
* @param password 密码
* @return 返回token
*/
@RequestMapping("/login")
public ResultEntity login(String username, String password) {
// 判断用户名是否注册
User user = userService.getUserByUsername(username);
if (user == null) {
return ResultEntity.error("用户名未注册");
}
// 比对密码
Boolean checkpw = PasswordUtils.checkpw(password, user.getPassword());
if (!checkpw){
return ResultEntity.error("用户名或者密码错误");
}
// 登陆成功 生成并返回token
Map<String, String> map = new HashMap<>();
map.put("username", username);
map.put("id", user.getId().toString());
String token = JWTUtils.createToken(map, 60 * 24 * 7);
return ResultEntity.success(token);
}
}
路由网关验证:
@RequestMapping("/auth")
@RestController
@Slf4j
public class AuthController {
// 网关的作用:拦截,转发,校验
// 因为自带了校验功能,因此像Cookie、Authorization等都已在网关验证屏蔽,需要在配置文件里打开并配置:
// zuul:
// sensitive-headers:
// 以上配置是将路由网关里的验证数组置为空,然后自己来作验证;
@RequestMapping("/getUserByToken")
public ResultEntity getUserByToken(@RequestHeader(name = "Authorization", required = false) String token) throws Exception {
log.info("token:{}", token);
// 检验token
DecodedJWT decodedJWT = JWTUtils.verify(token);
// 从token中获取用户的信息
String username = decodedJWT.getClaim("username").asString();
log.info("username:{}",username);
// 将用户信息返回给浏览器
return ResultEntity.success(username);
}
}
接口验证:
注册邮件接口测试:
邮件已发送:
本文章教学视频来自:https://www.bilibili.com/video/BV1tb4y1Q74E?p=3&t=125
静下心,慢慢来,会很快!