java电商项目搭建-------单点登录模块SSO

努力好了,时间会给你答案。--------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


静下心,慢慢来,会很快!

  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值