项目结构分析

项目结构:多模块(不是微服务)

整体思路:

  • 依赖链最底层:common,提供了公用的工具,定义了统一的controller,BaseEntity实体类公共对象,以及其他的操作工具
  • common的上一层:framework,主要是配置类,在这里配置了系统的一些东西,例如security aop 数据源的配置等等
  • 剩下的对应了三个的功能模块
    • system:系统管理,相等于具体的业务。比如系统管理中的用户管理,角色管理,菜单管理等等,包括系统监控什么的,都是写在system里面的
    • generator:代码生成
    • quartz:定时任务
  • ui:前端项目
  • admin:项目唯一的统一入口,controller都是在这里写的,上面的common等,都会被admin所依赖,这里的controller会调用对应的service

登录

验证码响应结果分析:

Base64字符串转图片:https://tool.jisuapi.com/base642pic.html

验证码生成接口分析:

url:localhost/dev-api/captchaImage

AjaxResult是一个封装的返回工具类

验证码配置分析

使用github上的kaptcha开源工具

导入maven依赖

1
2
3
4
5
<dependency>
<groupId>com.github.penggle</groupId>
<artifactId>kaptcha</artifactId>
<version>2.3.2</version>
</dependency>

接下来只要提供一个CaptchaConfig就行

再在里面提供一个bean

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
package com.lcdzzz.kaptcha;

import com.google.code.kaptcha.impl.DefaultKaptcha;
import com.google.code.kaptcha.util.Config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.Properties;

import static com.google.code.kaptcha.Constants.*;

@Configuration
public class CaptchaConfig {
@Bean(name = "captchaProducer")
public DefaultKaptcha getKaptchaBean() {
DefaultKaptcha defaultKaptcha = new DefaultKaptcha();
Properties properties = new Properties();
// 是否有边框 默认为true 我们可以自己设置yes,no
properties.setProperty(KAPTCHA_BORDER, "yes");
// 验证码文本字符颜色 默认为Color.BLACK
properties.setProperty(KAPTCHA_TEXTPRODUCER_FONT_COLOR, "black");
// 验证码图片宽度 默认为200
properties.setProperty(KAPTCHA_IMAGE_WIDTH, "160");
// 验证码图片高度 默认为50
properties.setProperty(KAPTCHA_IMAGE_HEIGHT, "60");
// 验证码文本字符大小 默认为40
properties.setProperty(KAPTCHA_TEXTPRODUCER_FONT_SIZE, "38");
// KAPTCHA_SESSION_KEY
//将来生成验证码时,自动将验证码文本存入session中
properties.setProperty(KAPTCHA_SESSION_CONFIG_KEY, "kaptchaCode");
// 验证码文本字符长度 默认为5
properties.setProperty(KAPTCHA_TEXTPRODUCER_CHAR_LENGTH, "4");
// 验证码文本字体样式 默认为new Font("Arial", 1, fontSize), new Font("Courier", 1, fontSize)
properties.setProperty(KAPTCHA_TEXTPRODUCER_FONT_NAMES, "Arial,Courier");
// 图片样式 水纹com.google.code.kaptcha.impl.WaterRipple 鱼眼com.google.code.kaptcha.impl.FishEyeGimpy 阴影com.google.code.kaptcha.impl.ShadowGimpy
properties.setProperty(KAPTCHA_OBSCURIFICATOR_IMPL, "com.google.code.kaptcha.impl.ShadowGimpy");
Config config = new Config(properties);
defaultKaptcha.setConfig(config);
return defaultKaptcha;
}
}

搞一个图片返回的controller

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

@RestController
public class CaptchaController {
@Autowired
DefaultKaptcha defaultKaptcha;
@GetMapping("/img")
public void captchaImg(HttpServletResponse reps) throws IOException {
//生成验证码的文本
String text = defaultKaptcha.createText();

//基于这个文本去生成一个图片对象。把这个文本作为参数传进去,它会绘制一个验证码图片出来
BufferedImage image = defaultKaptcha.createImage(text);

ImageIO.write(image,"jpg",reps.getOutputStream());
}
}

接下里我们访问localhost:8082/img

就会生成图片。

BUT!!!其实这个验证码工具,自己提供了个servlet,其实我们不需要自己写,直接用它提供的servlet

怎么用呢?

注册一个bean,原先的bean就不使用了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
@Bean
ServletRegistrationBean<HttpServlet> captchaServlet(){
ServletRegistrationBean<HttpServlet> bean = new ServletRegistrationBean<>();
bean.setServlet(new KaptchaServlet());
bean.addUrlMappings("/img2");
Properties properties = new Properties();
// 是否有边框 默认为true 我们可以自己设置yes,no
properties.setProperty(KAPTCHA_BORDER, "yes");
// 验证码文本字符颜色 默认为Color.BLACK
properties.setProperty(KAPTCHA_TEXTPRODUCER_FONT_COLOR, "black");
// 验证码图片宽度 默认为200
properties.setProperty(KAPTCHA_IMAGE_WIDTH, "160");
// 验证码图片高度 默认为50
properties.setProperty(KAPTCHA_IMAGE_HEIGHT, "60");
// 验证码文本字符大小 默认为40
properties.setProperty(KAPTCHA_TEXTPRODUCER_FONT_SIZE, "38");
// KAPTCHA_SESSION_KEY
//将来生成验证码时,自动将验证码文本存入session中
properties.setProperty(KAPTCHA_SESSION_CONFIG_KEY, "kaptchaCode");
// 验证码文本字符长度 默认为5
properties.setProperty(KAPTCHA_TEXTPRODUCER_CHAR_LENGTH, "4");
// 验证码文本字体样式 默认为new Font("Arial", 1, fontSize), new Font("Courier", 1, fontSize)
properties.setProperty(KAPTCHA_TEXTPRODUCER_FONT_NAMES, "Arial,Courier");
// 图片样式 水纹com.google.code.kaptcha.impl.WaterRipple 鱼眼com.google.code.kaptcha.impl.FishEyeGimpy 阴影com.google.code.kaptcha.impl.ShadowGimpy
properties.setProperty(KAPTCHA_OBSCURIFICATOR_IMPL, "com.google.code.kaptcha.impl.ShadowGimpy");

bean.setInitParameters(new HashMap<>((Map)properties));
return bean;
}

只有当:使用这个工具提供的验证码接口, properties.setProperty(KAPTCHA_SESSION_CONFIG_KEY, "kaptchaCode");这个配置才是有效的

此时,只有hello一个接口:

1
2
3
4
@GetMapping("/hello")
public void hello(HttpSession session){
System.out.println("session.getAttribute(\"kaptchaCode\") = " + session.getAttribute("kaptchaCode"));
}

结果:

=======总结========

properties.setProperty(KAPTCHA_SESSION_CONFIG_KEY, "kaptchaCode");

  • 如果想用Kaptcha本身提供的servlet来生成验证码的话,它就会自动把验证码文本存入session中
  • 但如果是自己写的接口,比如最开始的方法,那么文本就需要自己手动去存入session中了

tienchin项目本身

回归项目本身,其实properties.setProperty(KAPTCHA_SESSION_CONFIG_KEY, "kaptchaCode");这行配置没必要,不需要

为什么呢?

  • 生成验证码接口是我们自己写的
  • tienchin项目本身没有用session
  • 所以tienchin中Captconfig中的bean,最终目的就是生成字符串/数学运算的验证码

数学运算方面有意思的是这行代码,意思是验证码文本生成器是:KaptchaTextCreator

1
2
3
// 验证码文本生成器
properties.setProperty(KAPTCHA_TEXTPRODUCER_IMPL, "com.lcdzzz.tienchin.framework.config.KaptchaTextCreator");

登录

流程分析

  1. 登录核心:

    SysLoginService类下的login方法中的

    1
    2
    3
    4
    5
    // 该方法会去调用UserDetailsServiceImpl.loadUserByUsername
    // 这句话的意思就是去执行登录
    authentication = authenticationManager
    .authenticate(new UsernamePasswordAuthenticationToken(username, password));

  2. 登录成功后生成一个jwt字符串,return tokenService.createToken(loginUser);从这里点进createToken查看

  3. public String createToken(LoginUser loginUser) {
            String token = IdUtils.fastUUID();
            loginUser.setToken(token);
            setUserAgent(loginUser);
            refreshToken(loginUser);//所谓的刷新,就是存入到redis里面去
    
            Map<String, Object> claims = new HashMap<>();
            claims.put(Constants.LOGIN_USER_KEY, token);
            return createToken(claims);
        }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16

    4. 点进上面的refreshToken

    ```java
    /**
    * 刷新令牌有效期
    *
    * @param loginUser 登录信息
    */
    public void refreshToken(LoginUser loginUser) {
    loginUser.setLoginTime(System.currentTimeMillis());
    loginUser.setExpireTime(loginUser.getLoginTime() + expireTime * MILLIS_MINUTE);
    // 根据uuid将loginUser缓存
    String userKey = getTokenKey(loginUser.getToken());
    redisCache.setCacheObject(userKey, loginUser, expireTime, TimeUnit.MINUTES);
    }
  4. 暂且不考虑jwt原先的作用(例如无状态登录),其实这里的jwt作用已经“变形了”,实际上又变成了httpsession的作用,【虽然没有用session,但其实用户信息已经存入到redis里面了】

  5. 所谓的jwt,其实返回的仅仅只是uuid【相当于sessionid】,以后来回都传uuid

  6. 以后发送请求时,都会携带jwt字符串

  7. 登录成功后,f12查看token

这个token是一个jwt字符串,而jwt字符串是分为了三部分【都是用base64编码】,如下

1
2
3
eyJhbGciOiJIUzUxMiJ9 //指定算法名字
.eyJsb2dpbl91c2VyX2tleSI6ImYwOTg1ZjljLWQ0YmEtNDZlYy05MWZkLTFkZWE4ZmQxNTFkNCJ9 //核心信息==>json对象
.y_wTovMxjSdh2XDFFoS8Y_ANo2p3iYjrhU-rK72LfG63OrL1L-d05-NNNBBOSJsgA4G410uOYl8PustxtN3DaQ
  1. 使用base64解码工具【https://base64.us/】查看内容

流程分析总结

综上,登录流程就是:登录成功后,返回一个jwt字符串,以后每次发送请求都会携带它。jwt字符串分为三部分,第一部分是算法,第二部分是核心信息,核心信息里面只有一个uuid。

同时,它把用户信息存入redis里面中【登录信息,管理员,名字,leader,部门等等,都存在redis里面】,如图【左边相等于id,右边则是信息】

  • 以后每次访问接口时,都要带上jwt字符串
  • 所谓的jwt说白了就像用session时的jsessionid一样。只不过jsessionid是浏览器自动携带,而现在是自己手动加上jwt参数的

  • 每次请求都需要校验参数,而这个是在哪里校验的呢?

jwt校验

核心部分就是framework.security.filter下的JwtAuthenticationTokenFilter

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws ServletException, IOException {
LoginUser loginUser = tokenService.getLoginUser(request); //根据请求,来获取登录用户
if (StringUtils.isNotNull(loginUser) && /*这玩意为null,代表没有登录*/StringUtils.isNull(SecurityUtils.getAuthentication())) {
//令牌续签
tokenService.verifyToken(loginUser);//验证用户,看看有没有过期
//构建一个UsernamePasswordAuthenticationToken存放用户信息,然后存入SecurityContextHolder
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginUser, null, loginUser.getAuthorities());
authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
//此时SecurityContextHolder就有了用信息
//这样以后【接下来层级的过滤器,就不需要从请求头里获取用户信息了】
//当当前请求完毕时,SecurityContextHolder里的用户信息会被清楚掉。当下个请求来了,再周而复始,走来时路
}
chain.doFilter(request, response);
}

每次请求的时候,都带上jwt,然后拿着jwt,去redis里面查看用户信息,,然后存到SecurityContextHolder里面去

springsecurity登录配置分析

详见framework.config下的SecurityConfig类,以后准备再看看springsecurity

自定义动态数据源

思路分析

  1. 自定义一个注解@DataSource,将来可以将该注解加service层在方法或者类上面,表示方法或者类中的所有方法都使用某一个数据源
  2. 对于第一步,如果某个方法上面有@DataSource注解,那么就将该方法需要使用的数据源名称存入ThreadLocal。
  3. 自定义切面,在切面中解析@DataSource注解的时候,将@DataSource注解所标记的数据源存入到ThreadLocal中。
  4. 最后,当Mapper执行的时候,需要DataSource,他会自动去AbstractRoutingDataSource类中查找需要的数据源,我们只需要在AbstractRoutingDataSource中返回ThreadLocal中的值

综上:用@DataSource注解,在一个方法或者一个类上面,去标注你想使用哪个数据源。然后将来在这个AOP(切面)里面解析这个注解,把想使用的数据源的名字找出来,存在ThreadLocal里面去。当以后真正需要用的时候,人家会自动的从AbstractRoutingDataSource里面去查找需要的数据源。

所以,我们要做的就是:重写(自己写一个类继承)AbstractRoutingDataSource,然后在它的方法里面去返回ThreadLocal里边所存储的数据源的名字。最后它会根据名字找到对应的数据源

详细可见:自定义动态数据源

自定义限流注解

详见:自定义限流注解