【Java课设】组队系统【前后台分离+qq登录+jwt+邮箱推送】

274 阅读6分钟

前言

采用Erupt框架 + amis前台框架 快速搭建项目程序

【目前开放访问:前台链接】(没做完,但组队功能做完了)

  • 首先吹爆Erupt框架!!这个框架简直是后台开发利器,你甚至不需要先建表,直接拿来跑就出现了后台管理页面,利用代码生成反向生成实体类,直接映射生成了数据库,配合jpa,熟悉的话1小时不用一个简单的系统包括后台包括全套api就实现了。让你把时间放到具体业务上!!
  • 其次就是Amis低代码前端框架!!这个前台我是配合semantic一起弄的,我前台只是仅限于改点别人页面,这次第一次完全独立自己实现了一套定制的前台,虽然说不上很美观,但至少框架出来了。后面打算简单学下vue配合amis感觉可以起飞。

这两个框架现在github/gitee虽说不上热门,以我现有的开发感受来说,觉得是两个后面会起来的黑马。难在初学者入门,用起来后是真的起飞,下面简单看下我的开发过程吧。~

一、框架结构

目前项目开发周期:5月中旬到6.10号大半个月

主要是学习一些新技术:技术原理简单,但实际用起来还是不是一个概念层级的,断断续续把一些技术成功对接进来,也还有很多欠缺,后面还会继续完善这个项目的【项目跟进】

因为分离的项目,所以后面可能会用vue+amis重构前台,或者安卓手机端或者鸿蒙;

二、界面展示

流体式布局+电脑手机端适配

前台界面:

组队详细界面

通用后台管理框架

后台代码界面

实体类工具类控制层类拦截器及配置类

前台代码

Json配置界面主菜单主写jquery控制器

分布式部署/资源存放

静态资源存放github用cdn引用> 减轻前台服务器负担图片托管存gitee> 减轻后台服务器图片请求负担
前台代码存放Gitee仓库后台打jar包+mysql数据库部署自己服务器

三、数据库设计

ER流程图

省略属性

数据库关联

四、框架使用介绍

业务展示层:Erupt后台通用框架 + Amis百度前台通用框架 + Semantic通用前台组件库

交互规范层:[ restful接口规范 + 阿里java开发手册规范]


框架具体使用后面会在总结|总结后更新到下方

Erupt后台管理 -- 基于注解开发管理页面[详细 待更新]

Amis百度前台框架 -- 基于json开发前台界面[详细 待更新]

什么是 amis

amis 是一个低代码前端框架,它使用 JSON 配置来生成页面,可以减少页面开发工作量,极大提升效率。快速开发页面模板。

Semantic通用组件库[详细 待更新]

通过semantic提供好的丰富组件,基于amis构建的框架上构建内容,相当于amis是建房子,而semantic就是家具库。

五、技术点梳理

  1. 本系统仅采用qq登录,解决用户密码登录注册困难一问题,做到扫码即登录,登录即使用
  2. 技术点按照业务流程来梳

流程及技术点

qq登录 -- 用户绑定 -- 生成令牌 -- 组队请求[jwt校验 + 图片存储] -- 邮箱通知

qq登录[JustAuth支持]

业务流程

qq请求登录 -- 后台controller层封装返回qq登录链接 -- 前台跳转登录链接 -- 登录请求发送到腾讯 -- 腾讯验证后调用回调地址 --后台controller层响应处理数据绑定验证用户 -- 生成权限+用户打包发送给前台 -- 前台获取储存

绑定用户流程

相关代码:

    @RequestMapping("/callback/qq")
	//QQ登录回调请求
    public Result login(AuthCallback callback) {
        Object obj = null;
        try{
            AuthRequest authRequest = getAuthRequest();
            obj = authRequest.login(callback);
        }catch (Exception e){
            return Result.error("登录异常");
        }
		
        //通过qq回调返回后台捆绑用户
        JSONObject jsonObject = JSONObject.parseObject(JsonUtil.objectToJson(obj));
        JSONObject  data = JSONObject.parseObject(JsonUtil.objectToJson(jsonObject.get("data")));
        System.out.println(data);

        //验证用户
        User haveUser =null;
        User newUser = new User();
        haveUser = userRepository.findUserByUuid(data.getString("uuid"));
        if(haveUser!= null && haveUser.getId()!=null){
            newUser = haveUser;
        }else{
            newUser.setInstitute("这个人很懒,什么都没留下");
            newUser.setRegisterTime(new Date());
            newUser.setUuid(data.getString("uuid"));
            newUser.setGender(data.getString("gender"));
            newUser.setHeadImg(data.getString("avatar"));
            newUser.setNickName(data.getString("nickname"));
            newUser = userRepository.save(newUser);
        }
        //验证必要信息
        HashMap<String,Object> map = new HashMap<>();

        String token = UserJsonUtil.UserToJwt(newUser);
        map.put("token",token);
        map.put("user",newUser);

        if (newUser.getQq() == null || newUser.getEmail() == null || newUser.getRealName() == null
                || newUser.getUserId() == null) {
            System.out.println(map);
            return Result.build(200,"请补充必要信息",map);
        }
        return  Result.success(map);
        //JSONObject json = (JSONObject)JSONObject.toJSON(authRequest.login(callback));
        //处理权限
    }

困难点:

  1. qq开放平台审核注册 需要域名+审核
  2. 应用没上线前qq开放平台只支持审核域名回调,本地测试困难[解决方法用host 修改本地域名解析] + nginx端口转发实现

jwt+拦截认证

业务流程

给定能标识特定用户获取响应资源的内容body区 -- 通过密钥签名加密 -- 发送给用户 -- 用户需要请求数据时携带headers里面发送 -- 服务器解析确认返回相应请求

解决技术问题

传统项目存在用户认证问题,关于怎么让服务器识别用户,存服务器的session会增加服务器负担,简单存用户端的cookie容易被篡改;

需要一种解决方案那就是jwt,通过存放密钥在服务端和加密处理,发送给用户的令牌token唯一且不能被篡改且只有服务端有密钥匹配。

相关代码

签发token[校验和生成省略,可自行百度]

public static String createJWT(String id, String subject, long ttlMillis) {
    SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
    long nowMillis = System.currentTimeMillis();
    Date now = new Date(nowMillis);
    SecretKey secretKey = generalKey();
    JwtBuilder builder = Jwts.builder().setId(id).setSubject(subject) // 主题
        .setIssuer("404name") // 签发者
        .setIssuedAt(now) // 签发时间
        .signWith(signatureAlgorithm, secretKey); // 签名算法以及密匙
    if (ttlMillis >= 0) {
        long expMillis = nowMillis + ttlMillis;
        Date expDate = new Date(expMillis);
        builder.setExpiration(expDate); // 过期时间
    }
    return builder.compact();
}

拦截认证

通过拦截器拦截每一个请求校验header是否通过

配置intercaptor拦截所有请求

public class AuthInterceptor implements HandlerInterceptor {
    @Autowired
    private UserRepository userRepository;
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws IOException {
        //配置拦截器实现放行逻辑
        return true;
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) {
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {

    }
    //返回错误信息
    private static void setReturn(HttpServletResponse response, int status, String msg) throws IOException {
        
    }

}

配置config放行指定路径不经过header校验

@Configuration
public class InterceptorConfig implements WebMvcConfigurer {


    @Bean
    public AuthInterceptor authInterceptor() {
        return new AuthInterceptor();
    }

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        // 放行路径
        List<String> patterns = new ArrayList();
        registry.addInterceptor(authInterceptor())
                .addPathPatterns("/api/**")
                .excludePathPatterns("/api/login")
                .excludePathPatterns("/api/get/**");
    }
}

图片存储

指定外部oss存储[阿里云、七牛云、git仓库]

这里选择gitee仓库,先开好仓库 -- 配置密钥 -- java模拟http请求发送上传请求 -- 返回访问链接存数据库

    @Override
    public String upLoad(InputStream inputStream, String path) {
        //根据存储地区创建上传对象
        System.out.println(inputStream + "  " + path);
        //设置转存到Gitee仓库参数
        String paramImgFile = Base64.encode(inputStream);
        Map<String, Object> paramMap = new HashMap<String, Object>();
        paramMap.put("access_token", GiteeImgBed.ACCESS_TOKEN);
        paramMap.put("message", GiteeImgBed.ADD_MESSAGE);
        paramMap.put("content", paramImgFile);
        //转存文件路径
        String targetDir =GiteeImgBed.PATH +  path;
        String requestUrl = String.format(GiteeImgBed.CREATE_REPOS_URL, GiteeImgBed.OWNER,
                GiteeImgBed.REPO_NAME, targetDir);
        System.out.println("Url" + requestUrl);
        //工具类模拟post请求
        String resultJson = HttpUtil.post(requestUrl, paramMap);
        return getUrlFromJson(resultJson);//自己实现从json中提取指定的字符串
    }

邮件发送****

模拟请求即可,配置email工具类-- 提前开放邮箱email端口 -- 配置好邮箱模板 -- 封装使用即可;

EmailUtil.sendemail(接受邮箱,标题,内容模板);

效果:

六、遇到的问题及解决

跨域问题

一、问题:

跨域请求中包含自定义header字段时,浏览器console报错。

Request header field xfilesize is not allowed by Access-Control-Allow-Headers

二、原因:

包含自定义header字段的跨域请求,浏览器会先向服务器发送OPTIONS请求,探测该服务器是否允许自定义的跨域字段。

如果允许,则继续实际的POST/GET正常请求,否则,返回标题所示错误。

所以需要在后台添加过滤器对他进行回应

package com.jfly.apps.restful.springmvc;

import java.io.IOException;

import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletResponse;

public class CORSInterceptor implements Filter {

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
    }

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain)
            throws IOException, ServletException {
        HttpServletResponse response = (HttpServletResponse) servletResponse;
        String origin = (String) servletRequest.getRemoteHost() + ":" + servletRequest.getRemotePort();
        response.setHeader("Access-Control-Allow-Origin", "*");
        response.setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE");
        response.setHeader("Access-Control-Max-Age", "3600");
        response.setHeader("Access-Control-Allow-Headers", "x-requested-with,authorization");
        response.setHeader("Access-Control-Allow-Credentials", "true");
        filterChain.doFilter(servletRequest, servletResponse);
        
    }

    @Override
    public void destroy() {
    }
}

且再启动函数前面加上注解@ServletComponentScan扫描此过滤器即可。