改进之前做的小项目
需求
1、优化前端页面(尽量居中显示)
2、加入Redis
3、加入图形验证码
实现过程
1)service层优化
之前直接在controller层中使用UserMapper,不符合开发规范。现在要在service层调用UserMapper,然后在controller层中调用Service层。
改进后的目录结构
UserService
public interface UserService {
List<User> searchAllUsers();
int searchId(int id);
int searchByName(String name);
}
UserServiceImpl
public class UserServiceImpl implements UserService {
@Autowired
UserMapper userMapper;
//搜索所有的User
@Override
public List<User> searchAllUsers() {
List<User> userList = userMapper.selectList(null);
return userList;
}
//搜索当前自增Id
@Override
public int searchId(int id) {
return userMapper.searchId();
}
//通过username查找user,返回结果为人数
@Override
public int searchByName(String username) {
return userMapper.searchByName(username);
}
}
2)通用返回格式类
在实际开发中,后端通常只返回给前端一个json格式的数据串,所有的信息都包含在这条json串中,一般会自定义一种数据格式用来保存需要传递的信息,其格式通常包含状态码,状态信息,以及数据。
在entity包中新建类ResultData
//此类为通用的返回格式
@Data
@AllArgsConstructor
@NoArgsConstructor
public class ResultData implements Serializable {
public static final int SUCCESS_CODE = 200;
public static final int FAIL_CODE = 0;
public static final int ERROR_CODE = -1;
public static final String SUCCESS_MSG = "成功";
public static final String FAIL_MSG = "业务失败";
public static final String ERROR_MSG = "系统异常";
private Integer code;
private String msg;
private List<?> data;
public static ResultData success(){
return new ResultData(SUCCESS_CODE,SUCCESS_MSG,null);
}
public static ResultData success(String msg,List<?> data){
return new ResultData(SUCCESS_CODE,msg,data);
}
public static Object success(String msg) {
return new ResultData(SUCCESS_CODE,msg,null);
}
public static ResultData fail(){
return new ResultData(FAIL_CODE,FAIL_MSG,null);
}
public static ResultData fail(String msg,List<?> data){
return new ResultData(FAIL_CODE,msg,data);
}
public static ResultData fail(String s) {
return new ResultData(FAIL_CODE,s,null);
}
public static ResultData error(){
return new ResultData(ERROR_CODE,ERROR_MSG,null);
}
}
将通用的返回格式定义好后,对之前的代码进行优化,将大部分的返回格式定义为如下的形式
return JSON.toJSONString(ResultData.fail("验证码输入错误,请重试"));
其controller方法上应当增加注解@ResponseBody
由于大部分方法的返回格式都为String,所以还可以将以上的结果进一步封装,这里不做演示
3)加入验证码
这里使用的第三方包是kaptcha,这里的步骤参考了https://zhuanlan.zhihu.com/p/280784782
首先引入依赖
<dependency>
<groupId>com.github.penggle</groupId>
<artifactId>kaptcha</artifactId>
<version>2.3.2</version>
</dependency>
然后配置类
@Configuration
public class KaptchaConfig {
@Bean
public DefaultKaptcha getDefaultKaptcha(){
DefaultKaptcha captchaProducer = new DefaultKaptcha();
Properties properties = new Properties();
properties.setProperty("kaptcha.border", "no");
properties.setProperty("kaptcha.border.color", "105,179,90");
properties.setProperty("kaptcha.textproducer.font.color", "blue");
properties.setProperty("kaptcha.image.width", "110");
properties.setProperty("kaptcha.image.height", "36");
properties.setProperty("kaptcha.textproducer.font.size", "30");
properties.setProperty("kaptcha.session.key", "code");
properties.setProperty("kaptcha.textproducer.char.length", "4");
properties.setProperty("kaptcha.textproducer.font.names", "宋体,楷体,微软雅黑");
properties.setProperty("kaptcha.textproducer.char.string", "0123456789ABCEFGHIJKLMNOPQRSTUVWXYZ");
properties.setProperty("kaptcha.obscurificator.impl", "com.google.code.kaptcha.impl.WaterRipple");
properties.setProperty("kaptcha.noise.color", "black");
// properties.setProperty("kaptcha.noise.impl", "com.google.code.kaptcha.impl.DefaultNoise");
properties.setProperty("kaptcha.noise.impl", "com.google.code.kaptcha.impl.NoNoise");
properties.setProperty("kaptcha.background.clear.from", "232,240,254");
properties.setProperty("kaptcha.background.clear.to", "232,240,254");
properties.setProperty("kaptcha.textproducer.char.space", "3");
Config config = new Config(properties);
captchaProducer.setConfig(config);
return captchaProducer;
}
}
在配置类中可以配置验证码的大小、颜色、字体等等
写kaptchaController,用于生成验证码
@Controller
public class KaptchaController {
@Autowired
private Producer kaptchaProducer = null;
//生成
@RequestMapping("/kaptcha")
public void getKaptchaImage(HttpServletRequest request, HttpServletResponse response) throws IOException {
HttpSession session = request.getSession();
response.setDateHeader("Expires", 0);
response.setHeader("Cache-Control", "no-store, no-cache, must-revalidate");
response.addHeader("Cache-Control", "post-check=0, pre-check=0");
response.setHeader("Pragma", "no-cache");
response.setContentType("image/jpeg");
//生成验证码
String capText = kaptchaProducer.createText();
session.setAttribute(Constants.KAPTCHA_SESSION_KEY, capText);
//向客户端写出
BufferedImage bi = kaptchaProducer.createImage(capText);
try(ServletOutputStream out = response.getOutputStream()){
ImageIO.write(bi, "jpg", out);
}
}
}
随后修改前端页面,login.html之前使用的是表单提交,现在改为普通的table,登录及注册功能绑定点击事件,现在要将验证码加入,需要在table中新增一行
另外,之前用户名或者密码输入错误时,直接跳转到error界面,现在此界面上的操作都由ajax完成,具体的修改如下:
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>login</title>
</head>
<body>
<table align="center" style="vertical-align: middle">
<tr>
<td>用户名:</td>
<td><input type="text" id="username"></td>
</tr>
<tr>
<td>密码:</td>
<td><input type="password" id="password"></td>
</tr>
<tr>
<td>
<span class="input-group-btn">
<img id="captcha_img" alt="验证码" title="点击更换" onclick="refreshKaptcha()" src="/kaptcha" />
</span>
</td>
<td class="input-group">
<input class="form-control" type="text" autocomplete="new-password" placeholder="验证码" required maxlength="4"
id="verifyCode">
</td>
</tr>
<tr>
<td align="center"><button onclick="login()">登录</button></td>
<td align="center"><button onclick="register()">注册</button></td>
</tr>
</table>
</body>
</html>
<script>
function login() {
var username = document.getElementById("username").value;
var password = document.getElementById("password").value;
var verifyCode = document.getElementById("verifyCode").value;
$.ajax({
type: "POST",
url: "http://localhost:8080/login",
data: {username: username, password: password,verifyCode: verifyCode},
success: function(response) {
var res = JSON.parse(response);
window.alert(res.msg);
if(res.code === 200){
window.location.href = "http://localhost:8080/userList/";
}
// 处理成功响应的回调函数
},
error: function(xhr, status, error) {
window.alert("系统异常,登录失败");
window.location.href = "http://localhost:8080/";
// 处理错误响应的回调函数
}
});
}
function register() {
var username = document.getElementById("username").value;
var password = document.getElementById("password").value;
var verifyCode = document.getElementById("verifyCode").value;
$.ajax({
type: "POST",
url: "http://localhost:8080/register",
data: {username: username, password: password,verifyCode: verifyCode},
success: function(response) {
var res = JSON.parse(response);
window.alert(res.msg);
if(res.code === 200){
window.location.href = "http://localhost:8080/userList";
}
// 处理成功响应的回调函数
},
error: function(xhr, status, error) {
window.alert("系统异常,注册失败");
window.location.href = "http://localhost:8080/";
// 处理错误响应的回调函数
}
});
}
//刷新验证码
function refreshKaptcha() {
document.getElementById('captcha_img').src="/kaptcha?"+ Math.random();
}
</script>
同时,修改controller中对应的方法
register
@PostMapping("/register")
@ResponseBody
public String register(@RequestParam("username") String username,
@RequestParam("password") String password,
@RequestParam("verifyCode") String verifyCode,
Model model,
HttpServletResponse response,
HttpServletRequest request){
//非空验证
if(StringUtils.isEmptyOrWhitespace(username) || StringUtils.isEmptyOrWhitespace(password)) {
return JSON.toJSONString(ResultData.fail("用户名或密码不能为空,请重试"));
}
User user = new User();
user.setUsername(username);
user.setPassword(password);
//检验验证码输入
String kaptchaCode = (String) request.getSession().getAttribute(com.google.code.kaptcha.Constants.KAPTCHA_SESSION_KEY);
if(!StringUtils.equalsIgnoreCase(verifyCode,kaptchaCode)){
return JSON.toJSONString(ResultData.fail("验证码输入错误,请重试"));
}
//验证用户名
int n = userService.searchByName(username);
if(n > 0){
return JSON.toJSONString(ResultData.fail("该用户已存在,请重试"));
}
//插入数据库
int res = userService.insert(user);
//获取自增ID
int id = userService.searchId();
user.setId(id);
if(res > 0){
Cookie cookie = new Cookie("userId",id+"");
cookie.setMaxAge(60 * 60 * 24 * 7);//7天有效
response.addCookie(cookie);
return JSON.toJSONString(ResultData.success("注册成功",userService.searchAllUsers()));
}else{
return JSON.toJSONString(ResultData.fail("系统异常,请重试"));
}
}
login
@PostMapping("/login")
@ResponseBody
public String login(@RequestParam("username") String username,
@RequestParam("password") String password,
@RequestParam("verifyCode") String verifyCode,
Model model,
HttpServletResponse response,
HttpServletRequest request){
//非空验证
if(StringUtils.isEmptyOrWhitespace(username) || StringUtils.isEmptyOrWhitespace(password)) {
return JSON.toJSONString(ResultData.fail("用户名或密码不能为空,请重试"));
}
//检验验证码输入
String kaptchaCode = (String) request.getSession().getAttribute(com.google.code.kaptcha.Constants.KAPTCHA_SESSION_KEY);
if(!StringUtils.equalsIgnoreCase(verifyCode,kaptchaCode)){
return JSON.toJSONString(ResultData.fail("验证码输入错误,请重试"));
}
List<User> userList = userService.searchAllUsers();
//取出每一个user与输入进行对比
for(User user : userList){
if(user.getUsername().equals(username) && user.getPassword().equals(password)) {
Cookie cookie = new Cookie("userId",user.getId()+"");
cookie.setMaxAge(60 * 60 * 24 * 7);
response.addCookie(cookie);
return JSON.toJSONString(ResultData.success("登录成功",userService.searchAllUsers()));
}
}
return JSON.toJSONString(ResultData.fail("用户名或密码错误,请重试"));
}
4)目前的效果
登录界面
输入为空
登陆成功
用户列表
修改密码
5)加入Redis
步骤和kaptcha相似
1、引入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
2、配置类
//Redis配置类
@Configuration
@EnableCaching // 开启缓存支持
public class RedisConfig extends CachingConfigurerSupport {
@Resource
private LettuceConnectionFactory lettuceConnectionFactory;
/**
* RedisTemplate配置
*/
@Bean
public RedisTemplate<String, Object> redisTemplate(LettuceConnectionFactory lettuceConnectionFactory) {
// 设置序列化
Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<Object>(Object.class);
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(om);
// 配置redisTemplate
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<String, Object>();
redisTemplate.setConnectionFactory(lettuceConnectionFactory);
RedisSerializer<?> stringSerializer = new StringRedisSerializer();
redisTemplate.setKeySerializer(stringSerializer);// key序列化
redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);// value序列化
redisTemplate.setHashKeySerializer(stringSerializer);// Hash key序列化
redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer);// Hash value序列化
redisTemplate.afterPropertiesSet();
return redisTemplate;
}
/**
* 缓存配置管理器
*/
@Bean
public CacheManager cacheManager(LettuceConnectionFactory factory) {
// 配置序列化
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig().entryTtl(Duration.ofHours(1));
RedisCacheConfiguration redisCacheConfiguration = config.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()))
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()));
// 以锁写入的方式创建RedisCacheWriter对象
//RedisCacheWriter writer = RedisCacheWriter.lockingRedisCacheWriter(factory);
// 创建默认缓存配置对象
/* 默认配置,设置缓存有效期 1小时*/
//RedisCacheConfiguration defaultCacheConfig = RedisCacheConfiguration.defaultCacheConfig().entryTtl(Duration.ofHours(1));
/* 配置test的超时时间为120s*/
RedisCacheManager cacheManager = RedisCacheManager.builder(RedisCacheWriter.lockingRedisCacheWriter(factory)).cacheDefaults(redisCacheConfiguration)
.withInitialCacheConfigurations(singletonMap("test", RedisCacheConfiguration.defaultCacheConfig().entryTtl(Duration.ofMinutes(120)).disableCachingNullValues()))
.transactionAware().build();
return cacheManager;
}
}
3、springboot配置
spring.redis.host=192.168.148.132//redis服务器IP
spring.redis.port=6379//redis服务器端口
spring.redis.database=0
spring.redis.timeout=1800000
spring.redis.lettuce.pool.max-active=20
spring.redis.lettuce.pool.max-wait=-1
spring.redis.lettuce.pool.max-idle=5
spring.redis.lettuce.pool.min-idle=0
4、修改Controller
登录模块
@Autowired
RedisTemplate redisTemplate;
@PostMapping("/login")
@ResponseBody
public String login(@RequestParam("username") String username,
@RequestParam("password") String password,
@RequestParam("verifyCode") String verifyCode,
Model model,
HttpServletResponse response,
HttpServletRequest request){
//省略部分格式审查代码
//从Redis查
ValueOperations opsForValue = redisTemplate.opsForValue();
User user = JSON.parseObject((String)opsForValue.get(username),User.class);
if(user == null || !user.getPassword().equals(password)){
//如果从Redis找不到再从数据库找
user = userService.searchUserByName(username);
//还是找不到,返回结果
if(user == null){
return JSON.toJSONString(ResultData.fail("用户名或密码错误,请重试"));
}else{
//在数据库中找到,并更新到redis
opsForValue.set(username,user);
opsForValue.getAndExpire(username,7, TimeUnit.DAYS);//设置失效日期
return JSON.toJSONString(ResultData.success("登录成功",userService.searchAllUsers()));
}
}else{
opsForValue.getAndExpire(username,7, TimeUnit.DAYS);//更新失效日期
return JSON.toJSONString(ResultData.success("登录成功",userService.searchAllUsers()));
}
}
注册模块
@PostMapping("/register")
@ResponseBody
public String register(@RequestParam("username") String username,
@RequestParam("password") String password,
@RequestParam("verifyCode") String verifyCode,
Model model,
HttpServletResponse response,
HttpServletRequest request){
//省略代码
//插入数据库
int res = userService.insert(user);
//获取自增ID
int id = userService.searchId();
user.setId(id);
//插入Redis(此部分为新增代码)
ValueOperations opsForValue = redisTemplate.opsForValue();
opsForValue.set(username,JSON.toJSONString(user));//数据结构:key:username value:user对象
opsForValue.getAndExpire(username, 7,TimeUnit.DAYS);//7天过期
//省略代码
}
6)思考
以上已经完成了3个需求的实现,但是有些地方做的还不够好,如权限验证还没做、前端页面只做到水平集中,没有垂直居中等