一 会话管理登录功能
前置了解
初识cookie
如下图,浏览器初次访问服务器时,服务器生成数据将数据存放在浏览器的cookie中,当浏览器再次访问服务器时将会携带该cookie,此时服务器就可以确定该浏览器的身份了。
session的使用
如下图,session的实现需要借助cookie,不同cookie的是,session的数据是存放在服务器端。而此时的cookie只是用于传递数据。
二 分布式部署项目
在分布式项目中使用session不是一个好的解决方案,下面是几种实现方案。
-
使用粘性的化
-
使用同步
-
存在数据库或者rdis中
而本项最终是存放在redis中
三 生成验证码
生成验证码使用 Kaptcha
- 导入jar包
- 编写 Kaptcha 配置类
- 生成随机字符,生成图片
官网地址
<!-- https://mvnrepository.com/artifact/com.github.penggle/kaptcha -->
<dependency>
<groupId>com.github.penggle</groupId>
<artifactId>kaptcha</artifactId>
<version>2.3.2</version>
</dependency>
controller
@Autowired
private Producer kaptchaProducer;
private static final Logger logger = LoggerFactory.getLogger(LoginController.class);
/**
* 发送验证码
* @param response 向浏览器响应图片
* @param session 用于存放验证码
*/
@GetMapping("/kaptcha")
public void getKaptcha(HttpServletResponse response, HttpSession session) {
// 生成验证码
String text = kaptchaProducer.createText();
BufferedImage image = kaptchaProducer.createImage(text);
//将验证码存入到session中
session.setAttribute("kaptcha", text);
//将图片输出给浏览器
response.setContentType("image/png");
OutputStream os = null;
try {
os = response.getOutputStream();
ImageIO.write(image, "png", os);
} catch (IOException e) {
logger.error("响应验证码失败:" + e.getMessage());
}
}
工具包
package com.wjiangquan.community.config;
import com.google.code.kaptcha.Producer;
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;
/**
* @author weijiangquan
* @date 2022/10/4 -19:34
* @Description
*/
@Configuration
public class KaptchaConfig {
@Bean
public Producer kaptchProducer(){
Properties properties = new Properties();
properties.setProperty("kaptcha.image.width","100"); //设置图片的宽
properties.setProperty("kaptcha.image.height","40");
properties.setProperty("kaptcha.textproducer.font.size","32"); //单位像素
properties.setProperty("kaptcha.textproducer.font.color","0,0,0"); //单位像素
properties.setProperty("kaptcha.textproducer.char.string","9876543210qwertyuiopasdfghjklzxcfvbnm"); //单位像素
properties.setProperty("kaptcha.textproducer.char.length","4");
properties.setProperty("kaptcha.noise.impl","com.google.code.kaptcha.impl.NoNoise"); //生成干容东西(比如阴影)
DefaultKaptcha kaptcha = new DefaultKaptcha();
Config config = new Config(properties);
kaptcha.setConfig(config);
return kaptcha;
}
}
页面实现的代码
<div class="col-sm-4">
<img id="kaptcha" th:src="@{/kaptcha}" style="width:100px;height:40px;" class="mr-2"/>
<a href="javascript:refresh_kaptcha();" class="font-size-12 align-bottom">刷新验证码</a>
</div>
<script>
CONTEXT_PATH = "/community";
function refresh_kaptcha() {
// /kaptcha?p="+Math.random() 加?是为了欺骗浏览器的作用,达到刷新的作用
var path = CONTEXT_PATH + "/kaptcha?p="+Math.random();
$("#kaptcha").attr("src",path);
}
</script>
四 登录和退出的功能
下面是具体的实现步骤
-
书写实体类
LoginTicket
-
数据访问层 dao
-
插入数据
对于插入可以 通过
@Options(userGenerateKeys = true,keyProperty="id")
自动注入属性 -
通过 ticket查询数据
-
修改 ticket 修改状态 status
在注解中也可以像 mapper.xml 文件中一样实现动态 sql
- 测试dao层的增删改查
- 业务层 service
整体框架
public Map<String,Object> login(String username,String password,int expiredSecond(过期秒数)){
Map<String,Object> map = new HashMap();
- 处理空值
- 验证
- 验证账号是否为空
- 验证账号是否激活
- 验证密码
- 生成登录凭证
- 检查验证码
retrun null;
}
具体的代码实现
页面主要代码
<form class="mt-5" method="post" th:action="@{/login}">
<div class="form-group row">
<label for="username" class="col-sm-2 col-form-label text-right">账号:</label>
<div class="col-sm-10">
<input type="text" th:class="|form-control ${usernameMsg!=null?'is-invalid':''}|" id="username" name="username" th:value="${param.username}" placeholder="请输入您的账号!" required>
<div class="invalid-feedback" th:text="${usernameMsg}">
该账号不存在!
</div>
</div>
</div>
<div class="form-group row mt-4">
<label for="password" class="col-sm-2 col-form-label text-right">密码:</label>
<div class="col-sm-10">
<input type="password" th:class="|form-control ${passwordMsg!=null?'is-invalid':''}|" id="password" name="password" th:value="${param.password}" placeholder="请输入您的密码!" required>
<div class="invalid-feedback" th:text="${passwordMsg}">
密码长度不能小于8位!
</div>
</div>
</div>
<div class="form-group row mt-4">
<label for="verifycode" class="col-sm-2 col-form-label text-right">验证码:</label>
<div class="col-sm-6">
<input type="text" th:class="|form-control ${codeMsg!=null?'is-invalid':''}|" id="verifycode" name="code" placeholder="请输入验证码!">
<div class="invalid-feedback" th:text="${codeMsg}">
验证码不正确!
</div>
</div>
<div class="col-sm-4">
<img id="kaptcha" th:src="@{/kaptcha}" style="width:100px;height:40px;" class="mr-2"/>
<a href="javascript:refresh_kaptcha();" class="font-size-12 align-bottom">刷新验证码</a>
</div>
</div>
<div class="form-group row mt-4">
<div class="col-sm-2"></div>
<div class="col-sm-10">
<input type="checkbox" id="remember-me" th:checked="${param.rememberMe}" name="rememberMe">
<label class="form-check-label" for="remember-me">记住我</label>
<a href="forget.html" class="text-danger float-right">忘记密码?</a>
</div>
</div>
<div class="form-group row mt-4">
<div class="col-sm-2"></div>
<div class="col-sm-10 text-center">
<button type="submit" class="btn btn-info text-white form-control">立即登录</button>
</div>
</div>
</form>
controller的代码
/**
* @param username 用户名
* @param password 密码
* @param code 验证码
* @param rememberMe 记住我
* @param model 传给模板的数据
* @param session 用户获取存入的验证码用于验证
* @param response 将 ticket 通过该对象放入session中
* @return 处理完之后前往的页面(成功前往首页,失败重定向回登录界面)
*/
@RequestMapping(path = "/login",method = RequestMethod.POST)
public String login(@RequestParam("username") String username,
@RequestParam("password") String password,
@RequestParam(value = "code",required = false) String code,
@RequestParam(value = "rememberMe",defaultValue = "false") boolean rememberMe,
Model model,
HttpSession session,
HttpServletResponse response){
//1.检测验证码是否正确
String kaptcha = (String)session.getAttribute("kaptcha");
if(StringUtils.isBlank(code)||StringUtils.isBlank(kaptcha)||!code.equals(kaptcha)){
model.addAttribute("codeMsg","验证码不正确");
return "/site/login";
}
//2.检查账号密码(交给业务层进行管理)
//设置超时的秒数
int expiredSecond = rememberMe ? REMEMBER_EXPIRED_SECONDS : DEFAULT_EXPIRED_SECONDS;
Map<String, Object> map = userService.login(username, password, expiredSecond);
//成功前往首页,不成功继续在登录界面
if(map.containsKey("ticket")){
Cookie cookie = new Cookie("ticket",map.get("ticket").toString());
cookie.setPath(contextPath);
cookie.setMaxAge(expiredSecond);
response.addCookie(cookie);
return "redirect:/index";//直接重定向到首页
}else {
model.addAttribute("usernameMsg",map.get("usernameMsg"));
model.addAttribute("passwordMsg",map.get("passwordMsg"));
return "/site/login";
}
}
service
@Override
public Map<String,Object> login(String username,String password,int expiredSeconds){
Map<String,Object> map = new HashMap<>();
//1.验证空值
if(StringUtils.isBlank(username)){
map.put("usernameMsg","用户名不能为空");
return map;
}
if(StringUtils.isBlank(password)){
map.put("passwordMsg","密码不能为空");
return map;
}
//2.验证
// 验证账号
User userByName = userMapper.getUserByName(username);
if(userByName==null){
map.put("usernameMsg","用户名不能为空");
return map;
}
//验证是否激活
if(userByName.getStatus() == 0){
map.put("usernameMsg","账号没有激活!");
return map;
}
// 验证密码
String salt = userByName.getSalt();
String password1 = CommunityUtil.MD5(salt + password);
if(!userByName.getPassword().equals(password1)){
map.put("passwordMsg","密码不正确");
return map;
}
//3 登录成功(将ticket放入到login_ticket表中) 生成登录凭证
LoginTicket loginTicket = new LoginTicket();
loginTicket.setUserId(userByName.getId());
loginTicket.setStatus(0);
loginTicket.setTicket(CommunityUtil.generateUUID());
loginTicket.setExpired(new Date(System.currentTimeMillis() + expiredSeconds*1000));
loginTicketMapper.insertLoginTicket(loginTicket);
map.put("ticket",loginTicket.getTicket());
// loginTicketMapper.insertLoginTicket()
return map;
}
dao
@Mapper
public interface LoginTicketMapper {
@Insert({"insert into login_ticket(user_id,ticket,status,expired) ",
"values(#{userId},#{ticket},#{status},#{expired})"})
@Options(useGeneratedKeys = true,keyProperty = "id")
int insertLoginTicket(LoginTicket loginTicket);
@Select({"select * from login_ticket where ticket = #{ticket}"})
LoginTicket getByTicket(String ticket);
@Update({"update login_ticket set status = #{status} where ticket = #{ticket}"})
int updateByTicket(@Param("status") int status,@Param("ticket") String ticket);
}
技术要点总结
- 对于标记中的SpringMcv不会帮忙放到 model对象中,只有是对象时,spingMvc才会将其放入到model对象中
因此在数据进行会回显时,由于在一个请求中,即在一个 request
中,在页面中可以通过 param.username
方式获取 request
域中的东西。如下所示当想让用户名回显的时的使用方式