??如果對(duì)你有幫助的話??
??為博主點(diǎn)個(gè)贊吧 ??
??點(diǎn)贊是對(duì)博主最大的鼓勵(lì)??
??愛心發(fā)射~??
一、發(fā)送郵件
1、啟用客戶端SMTP服務(wù)
bofryuzursekbiab——密碼
2、導(dǎo)入jar包
<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-mail -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-mail</artifactId>
<version>2.7.0</version>
</dependency>
3、郵箱參數(shù)配置
- 訪問郵箱域名
- 郵箱端口
- 賬號(hào)
- 密碼
- 協(xié)議
- 詳細(xì)配置
# MailProperties
spring.mail.host=smtp.sina.com
spring.mail.port=465
spring.mail.username=@.com
spring.mail.password=nowcoder123
spring.mail.protocol=smtps
spring.mail.properties.mail.smtp.ssl.enable=true
MailClient
package com.nowcoder.community.util;
@Component
public class MailClient {
private static final Logger logger = LoggerFactory.getLogger(MailClient.class);
@Autowired
private JavaMailSender mailSender;
@Value("${spring.mail.username}")
private String from;
public void sendMail(String to, String subject, String content) {
try {
MimeMessage message = mailSender.createMimeMessage();
MimeMessageHelper helper = new MimeMessageHelper(message);
helper.setFrom(from);
helper.setTo(to);
helper.setSubject(subject);
helper.setText(content, true);
mailSender.send(helper.getMimeMessage());
} catch (MessagingException e) {
logger.error("發(fā)送郵件失敗:" + e.getMessage());
}
}
}
demo.html
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>郵件示例</title>
</head>
<body>
<p>歡迎你, <span style="color:red;" th:text="${username}"></span>!</p>
</body>
</html>
MailTests
@RunWith(SpringRunner.class)
@SpringBootTest
@ContextConfiguration(classes = CommunityApplication.class)
public class MailTests {
@Autowired
private MailClient mailClient;
@Autowired
private TemplateEngine templateEngine;
@Test
public void testTextMail() {
mailClient.sendMail("1724206051@qq.com", "TEST", "Welcome.");
}
@Test
public void testHtmlMail() {
Context context = new Context();
context.setVariable("username", "sunday");
String content = templateEngine.process("/mail/demo", context);
System.out.println(content);
mailClient.sendMail("1724206051@qq.com", "HTML", content);
}
}
總結(jié)
-
JavaMailSender
是Spring Email
的核心組件,負(fù)責(zé)發(fā)送郵件 -
MimeMessage
用于封裝郵件的相關(guān)信息 -
MimeMessageHelper
用于輔助構(gòu)建MimeMessage
對(duì)象 -
TemplateEngine
是模板引擎,負(fù)責(zé)格式化HTML
格式的郵件
Spring Boot
對(duì)發(fā)送郵件提供了支持,可以通過MailProperties
對(duì)郵件進(jìn)行配置
- 可以配置郵件服務(wù)器的域名和端口
- 可以配置發(fā)件人的賬號(hào)及密碼
- 可以配置發(fā)送郵件的協(xié)議類型
哪些會(huì)被Spring Boot
自動(dòng)裝配到Spring
容器中
JavaMailSender
TemplateEngine
二、開發(fā)注冊(cè)功能
1、訪問注冊(cè)頁面
點(diǎn)擊頂部區(qū)域內(nèi)的鏈接,打開注冊(cè)頁面。
修改——thymeleaf
首頁—超鏈接——index.html
每個(gè)html
頭部復(fù)用——index.html
LoginController
返回注冊(cè)頁面
@Controller
public class LoginController {
@Autowired
private UserService userService;
@RequestMapping(path = "/register", method = RequestMethod.GET)
public String getRegisterPage() {
return "/site/register";
}
}
2、提交注冊(cè)數(shù)據(jù)
通過表單提交數(shù)據(jù)。
添加依賴和配置
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.9</version>
</dependency>
# community
community.path.domain=http://localhost:8080
service層
- 服務(wù)端發(fā)送激活郵件。
工具類——CommunityUtil
生成隨機(jī)字符串
給文件生成隨機(jī)名字
public class CommunityUtil {
// 生成隨機(jī)字符串
public static String generateUUID() {
return UUID.randomUUID().toString().replaceAll("-", "");
}
// MD5加密 : 只能加密,不能解密
// hello -> abc123def456
// hello + 3e4a8 -> abc123def456abc
// 先加字符串 , 再加密
public static String md5(String key) {
// 參數(shù)為空,不加密
if (StringUtils.isBlank(key)) {
return null;
}
return DigestUtils.md5DigestAsHex(key.getBytes());
}
}
注入——UserService
- 注入郵件客戶端
- 注入模板引擎
@Service
public class UserService {
@Autowired
private UserMapper userMapper;
@Autowired
private MailClient mailClient; // 郵件客戶端
@Autowired
private TemplateEngine templateEngine; // 模板引擎
@Value("${community.path.domain}")
private String domain; // 域名
@Value("${server.servlet.context-path}")
private String contextPath; // 項(xiàng)目名
public User findUserById(int id) {
return userMapper.selectById(id);
}
public Map<String, Object> register(User user){
Map<String, Object> map = new HashMap<>(); // map實(shí)例化
// 空值處理
if (user == null) {
throw new IllegalArgumentException("參數(shù)不能為空!");
}
if (StringUtils.isBlank(user.getUsername())) {
map.put("usernameMsg", "賬號(hào)不能為空!");
return map;
}
if (StringUtils.isBlank(user.getPassword())) {
map.put("passwordMsg", "密碼不能為空!");
return map;
}
if (StringUtils.isBlank(user.getEmail())) {
map.put("emailMsg", "郵箱不能為空!");
return map;
}
// 驗(yàn)證賬號(hào)
User u = userMapper.selectByName(user.getUsername());
if (u != null) {
map.put("usernameMsg", "該賬號(hào)已存在!");
return map;
}
// 驗(yàn)證郵箱
u = userMapper.selectByEmail(user.getEmail());
if (u != null) {
map.put("emailMsg", "該郵箱已被注冊(cè)!");
return map;
}
// 注冊(cè)用戶
user.setSalt(CommunityUtil.generateUUID().substring(0, 5)); //生成隨機(jī)字符串
user.setPassword(CommunityUtil.md5(user.getPassword() + user.getSalt())); //密碼拼接
user.setType(0); // 類型
user.setStatus(0); //狀態(tài)
user.setActivationCode(CommunityUtil.generateUUID()); //激活碼
user.setHeaderUrl(String.format("http://images.nowcoder.com/head/%dt.png", new Random().nextInt(1000))); //頭像
user.setCreateTime(new Date()); // 創(chuàng)建時(shí)間
userMapper.insertUser(user); //添加庫里
// 激活郵件
Context context = new Context();
context.setVariable("email", user.getEmail());
// http://localhost:8081/community/activation/101/code
// 域名——項(xiàng)目名——功能訪問名 + 用戶 id 激活碼
String url = domain + contextPath + "/activation/" + user.getId() + "/" + user.getActivationCode();
context.setVariable("url", url);
String content = templateEngine.process("/mail/activation", context);
mailClient.sendMail(user.getEmail(), "激活賬號(hào)", content); // 標(biāo)題 內(nèi)容
return map;
}
}
牛客網(wǎng)隨機(jī)頭像
改造模板——activation.html
<!doctype html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="utf-8">
<link rel="icon" href="https://static.nowcoder.com/images/logo_87_87.png"/>
<title>??途W(wǎng)-激活賬號(hào)</title>
</head>
<body>
<div>
<p>
<b th:text="${email}">xxx@xxx.com</b>, 您好!
</p>
<p>
您正在注冊(cè)??途W(wǎng), 這是一封激活郵件, 請(qǐng)點(diǎn)擊
<a th:href="${url}">此鏈接</a>,
激活您的??唾~號(hào)!
</p>
</div>
</body>
</html>
控制層
- 服務(wù)端驗(yàn)證賬號(hào)是否已存在、郵箱是否已注冊(cè)。
- 注冊(cè)成功——到首頁進(jìn)行激活——在登陸
@{}
:路徑是動(dòng)態(tài)的${}
:里邊是變量
注冊(cè)成功或有錯(cuò)誤返回——LoginController
@RequestMapping(path = "/register", method = RequestMethod.POST)
public String register(Model model, User user) {
Map<String, Object> map = userService.register(user);
if (map == null || map.isEmpty()) {
model.addAttribute("msg", "注冊(cè)成功,我們已經(jīng)向您的郵箱發(fā)送了一封激活郵件,請(qǐng)盡快激活!");
model.addAttribute("target", "/index");
return "/site/operate-result";
} else {
model.addAttribute("usernameMsg", map.get("usernameMsg"));
model.addAttribute("passwordMsg", map.get("passwordMsg"));
model.addAttribute("emailMsg", map.get("emailMsg"));
return "/site/register";
}
}
激活成功模板——operate-result.html
賬號(hào)、密碼、郵箱錯(cuò)誤——返回register.html
默認(rèn)值的顯示
3、激活注冊(cè)賬號(hào)
點(diǎn)擊郵件中的鏈接,訪問服務(wù)端的激活服務(wù)。
在service層加一個(gè)業(yè)務(wù),幾種情況:
- 激活成功
- 多次點(diǎn)擊激活鏈接
- 重復(fù)激活給提示
- 激活碼偽造
三種結(jié)果:
- 成功
- 重復(fù)激活
- 失敗
常量接口——CommunityConstant
public interface CommunityConstant {
/**
* 激活成功
*/
int ACTIVATION_SUCCESS = 0;
/**
* 重復(fù)激活
*/
int ACTIVATION_REPEAT = 1;
/**
* 激活失敗
*/
int ACTIVATION_FAILURE = 2;
}
UserService
public int activation(int userId, String code) {
User user = userMapper.selectById(userId);
// 看狀態(tài)、 激活碼
if (user.getStatus() == 1) {
return ACTIVATION_REPEAT;
} else if (user.getActivationCode().equals(code)) {
userMapper.updateStatus(userId, 1);
return ACTIVATION_SUCCESS;
} else {
return ACTIVATION_FAILURE;
}
}
返回頁面——LoginController
@RequestMapping(path = "/login", method = RequestMethod.GET)
public String getLoginPage() {
return "/site/login";
}
//處理請(qǐng)求
// http://localhost:8080/community/activation/101/code
@RequestMapping(path = "/activation/{userId}/{code}", method = RequestMethod.GET)
public String activation(Model model, @PathVariable("userId") int userId, @PathVariable("code") String code) {
int result = userService.activation(userId, code);
if (result == ACTIVATION_SUCCESS) {
model.addAttribute("msg", "激活成功,您的賬號(hào)已經(jīng)可以正常使用了!");
model.addAttribute("target", "/login");
} else if (result == ACTIVATION_REPEAT) {
model.addAttribute("msg", "無效操作,該賬號(hào)已經(jīng)激活過了!");
model.addAttribute("target", "/index");
} else {
model.addAttribute("msg", "激活失敗,您提供的激活碼不正確!");
model.addAttribute("target", "/index");
}
return "/site/operate-result";
}
將login
添加到模板
驗(yàn)證碼更改
首頁更改——index
激活,跳到登錄頁面
三、會(huì)話管理
HTTP教程
HTTP 是無狀態(tài),有會(huì)話的
HTTP 是無狀態(tài)的:在同一個(gè)連接中,兩個(gè)執(zhí)行成功的請(qǐng)求之間是沒有關(guān)系的。這就帶來了一個(gè)問題,用戶沒有辦法在同一個(gè)網(wǎng)站中進(jìn)行連續(xù)的交互,比如在一個(gè)電商網(wǎng)站里,用戶把某個(gè)商品加入到購物車,切換一個(gè)頁面后再次添加了商品,這兩次添加商品的請(qǐng)求之間沒有關(guān)聯(lián),瀏覽器無法知道用戶最終選擇了哪些商品。而使用 HTTP 的頭部擴(kuò)展,HTTP Cookies 就可以解決這個(gè)問題。把 Cookies 添加到頭部中,創(chuàng)建一個(gè)會(huì)話讓每次請(qǐng)求都能共享相同的上下文信息,達(dá)成相同的狀態(tài)。
1、HTTP Cookie
HTTP Cookie(也叫 Web Cookie 或?yàn)g覽器 Cookie)是服務(wù)器發(fā)送到用戶瀏覽器并保存在本地的一小塊數(shù)據(jù),它會(huì)在瀏覽器下次向同一服務(wù)器再發(fā)起請(qǐng)求時(shí)被攜帶并發(fā)送到服務(wù)器上。通常,它用于告知服務(wù)端兩個(gè)請(qǐng)求是否來自同一瀏覽器,如保持用戶的登錄狀態(tài)。Cookie 使基于無狀態(tài)的 HTTP 協(xié)議記錄穩(wěn)定的狀態(tài)信息成為了可能。
-
是服務(wù)器發(fā)送到瀏覽器,并保存在瀏覽器端的一小塊數(shù)據(jù)。
-
瀏覽器下次訪問該服務(wù)器時(shí),會(huì)自動(dòng)攜帶塊該數(shù)據(jù),將其發(fā)送給服務(wù)器。
-
識(shí)別瀏覽器、記住瀏覽器
-
下次再發(fā)到瀏覽器,會(huì)攜帶上次數(shù)據(jù)
好處:彌補(bǔ)HTTP無狀態(tài)時(shí)的情況,讓業(yè)務(wù)得以延續(xù)
缺點(diǎn):
- 存在客戶端,不安全
- 增加數(shù)據(jù)量,影響性能
- 瀏覽器訪問服務(wù)器,服務(wù)器會(huì)產(chǎn)生一個(gè)
Cookie
對(duì)象 - 服務(wù)器-返回——
Cookie
,其中攜帶數(shù)據(jù)(默認(rèn)在響應(yīng)的頭里),瀏覽器保存以下數(shù)據(jù) - 瀏覽器——服務(wù)器,
Cookie
在請(qǐng)求的頭里,服務(wù)器記住用戶
set cookie
// cookie示例
@RequestMapping(path = "/cookie/set", method = RequestMethod.GET)
@ResponseBody
public String setCookie(HttpServletResponse response) {
// 創(chuàng)建cookie
Cookie cookie = new Cookie("code", CommunityUtil.generateUUID());
// 設(shè)置cookie生效的范圍
cookie.setPath("/community/alpha");
// 設(shè)置cookie的生存時(shí)間
cookie.setMaxAge(60 * 10);
// 發(fā)送cookie
response.addCookie(cookie);
return "set cookie";
}
get cookie
@RequestMapping(path = "/cookie/get", method = RequestMethod.GET)
@ResponseBody
public String getCookie(@CookieValue("code") String code) {
System.out.println(code);
return "get cookie";
}
2、Session
- 是
JavaEE
的標(biāo)準(zhǔn),用于在服務(wù)端記錄客戶端信息。 - 數(shù)據(jù)存放在服務(wù)端更加安全,但是也會(huì)增加服務(wù)端的內(nèi)存壓力。
服務(wù)器靠什么區(qū)分Session
- 服務(wù)器——瀏覽器發(fā)送
cookie
,cookie
中攜帶Session
標(biāo)識(shí)
set Session
// session示例
@RequestMapping(path = "/session/set", method = RequestMethod.GET)
@ResponseBody
public String setSession(HttpSession session) {
session.setAttribute("id", 1);
session.setAttribute("name", "Test");
return "set session";
}
get session
// session示例
@RequestMapping(path = "/session/get", method = RequestMethod.GET)
@ResponseBody
public String getSession(HttpSession session) {
System.out.println(session.getAttribute("id"));
System.out.println(session.getAttribute("name"));
return "get session";
}
為什么在分布式部署下,Session用的少了?實(shí)際應(yīng)用中怎么解決
- 分布式部署——同時(shí)部署多臺(tái)服務(wù)器,同時(shí)向?yàn)g覽器提供支持
分布式部署下,有什么問題:
解決方法
1、粘性Session
每個(gè)瀏覽器始終分配給一臺(tái)服務(wù)器去處理,固定ip
給同一個(gè)服務(wù)器
缺點(diǎn):難以保證負(fù)載均衡,性能不好
2、同步Session
服務(wù)器之間同步Session
缺點(diǎn): 對(duì)服務(wù)器性能產(chǎn)生影響,服務(wù)器之間耦合
3、共享Session
單獨(dú)有一臺(tái)服務(wù)器來處理Session
缺點(diǎn):這臺(tái)服務(wù)器掛了都無法工作
4、能存cookie就存cookie,敏感數(shù)據(jù)可以存到數(shù)據(jù)庫里
優(yōu)點(diǎn):
- 很好的共享數(shù)據(jù)、同步數(shù)據(jù)
缺點(diǎn): - 傳統(tǒng)的關(guān)系型數(shù)據(jù)庫把數(shù)據(jù)存到硬盤,訪問數(shù)據(jù)到硬盤,性能慢
- 并發(fā)量大出現(xiàn)瓶頸
5、 可以存到非關(guān)系型數(shù)據(jù)庫 Redis
目前沒部署Redis,怎么辦?
- 適合存到MySQL,就存
- 不適合存到session
四、生成驗(yàn)證碼——Kaptcha
1、Kaptcha
Kaptcha官方手冊(cè)
- 導(dǎo)入
jar
包 - 編寫
Kaptcha
配置類 - 生成隨機(jī)字符、生成圖片
導(dǎo)入依賴
<dependency>
<groupId>com.github.penggle</groupId>
<artifactId>kaptcha</artifactId>
<version>2.3.2</version>
</dependency>
2、KaptchaConfig——定義驗(yàn)證碼圖片
package com.nowcoder.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;
@Configuration
public class KaptchaConfig {
@Bean
public Producer kaptchaProducer() {
Properties properties = new Properties();
properties.setProperty("kaptcha.image.width", "100"); // 圖片寬度
properties.setProperty("kaptcha.image.height", "40"); // 圖片高度
properties.setProperty("kaptcha.textproducer.font.size", "32"); // 字號(hào)
properties.setProperty("kaptcha.textproducer.font.color", "0,0,0"); // 顏色- 黑色
properties.setProperty("kaptcha.textproducer.char.string", "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYAZ"); // 隨機(jī)字符范圍
properties.setProperty("kaptcha.textproducer.char.length", "4"); // 長(zhǎng)度
properties.setProperty("kaptcha.noise.impl", "com.google.code.kaptcha.impl.NoNoise");
DefaultKaptcha kaptcha = new DefaultKaptcha(); // 默認(rèn)實(shí)現(xiàn)類
Config config = new Config(properties); // 配置
kaptcha.setConfig(config);
return kaptcha;
}
}
3、LoginController——生成驗(yàn)證碼
@Autowired
private Producer kaptchaProducer;
private static final Logger logger = LoggerFactory.getLogger(LoginController.class);
@RequestMapping(path = "/kaptcha", method = RequestMethod.GET)
public void getKaptcha(HttpServletResponse response, HttpSession session) {
// 生成驗(yàn)證碼
String text = kaptchaProducer.createText();
BufferedImage image = kaptchaProducer.createImage(text);
// 將驗(yàn)證碼存入session
session.setAttribute("kaptcha", text);
// 將突圖片輸出給瀏覽器
response.setContentType("image/png");
try {
OutputStream os = response.getOutputStream();
ImageIO.write(image, "png", os);
} catch (IOException e) {
logger.error("響應(yīng)驗(yàn)證碼失敗:" + e.getMessage());
}
}
4、刷新驗(yàn)證碼
結(jié)果
5、總結(jié)
關(guān)于Kaptcha的描述
-
Producer
是Kaptcha
的核心接口 -
DefaultKaptcha
是Kaptcha
核心接口的默認(rèn)實(shí)現(xiàn)類 -
Spring Boot
沒有為Kaptcha
提供自動(dòng)配置
關(guān)于使用Kaptcha的描述
- 可以通過
Producer
創(chuàng)建隨機(jī)的驗(yàn)證碼文本 - 可以傳入文本,讓
Producer
創(chuàng)建對(duì)應(yīng)的驗(yàn)證碼圖片 - 服務(wù)端需要將驗(yàn)證碼圖片輸出給瀏覽器
關(guān)于Kaptcha配置的描述
- 可以配置Kaptcha圖片的寬度、高度、字號(hào)、顏色
- 可以配置Kaptcha驗(yàn)證碼的字符范圍、字符個(gè)數(shù)
五、開發(fā)登錄、退出功能
訪問登錄頁面
- 點(diǎn)擊頂部區(qū)域內(nèi)的鏈接,打開登錄頁面。
1、數(shù)據(jù)庫——login_ticket
- id——主鍵
- user_id——用戶id
- ticket——憑證(唯一標(biāo)識(shí),唯一字符串)
- status——0 有效, 1 無效
- expired——過期時(shí)間
2、實(shí)體類——LoginTicket
getter and setter
toString
public class LoginTicket {
private int id;
private int userId;
private String ticket;
private int status;
private Date expired;
}
3、LoginTicketMapper——寫SQL、通過注解
依據(jù)ticket
,來查找
package com.nowcoder.community.dao;
import com.nowcoder.community.entity.LoginTicket;
import org.apache.ibatis.annotations.*;
@Mapper
public interface LoginTicketMapper {
@Insert({
"insert into login_ticket(user_id,ticket,status,expired) ",
"values(#{userId},#{ticket},#{status},#{expired})"
})
@Options(useGeneratedKeys = true, keyProperty = "id") // 希望主鍵自動(dòng)生成
int insertLoginTicket(LoginTicket loginTicket);
@Select({
"select id,user_id,ticket,status,expired ",
"from login_ticket where ticket=#{ticket}"
})
// 以 ticket 為憑證查詢
LoginTicket selectByTicket(String ticket);
@Update({
"<script>",
"update login_ticket set status=#{status} where ticket=#{ticket} ",
"<if test=\"ticket!=null\"> ",
"and 1=1 ",
"</if>",
"</script>"
})
int updateStatus(String ticket, int status);
}
4、測(cè)試
@Test
public void testInsertLoginTicket() {
LoginTicket loginTicket = new LoginTicket();
loginTicket.setUserId(101);
loginTicket.setTicket("abc");
loginTicket.setStatus(0);
loginTicket.setExpired(new Date(System.currentTimeMillis() + 1000 * 60 * 10));
loginTicketMapper.insertLoginTicket(loginTicket);
}
@Test
public void testSelectLoginTicket() {
LoginTicket loginTicket = loginTicketMapper.selectByTicket("abc");
System.out.println(loginTicket);
loginTicketMapper.updateStatus("abc", 1);
loginTicket = loginTicketMapper.selectByTicket("abc");
System.out.println(loginTicket);
}
登錄
- 驗(yàn)證賬號(hào)、密碼、驗(yàn)證碼。
- 成功時(shí),生成登錄憑證,發(fā)放給客戶端。
- 失敗時(shí),跳轉(zhuǎn)回登錄頁。
1、業(yè)務(wù)層——UserService
登錄失敗的原因:
- 賬號(hào)沒輸入、不存在、沒激活
用戶在頁面輸入的密碼是明文
// 用戶在頁面輸入的密碼是明文,存的是加密后的,MD5
// expiredSeconds 多長(zhǎng)時(shí)間后,憑證過期
public Map<String, Object> login(String username, String password, int expiredSeconds) {
Map<String, Object> map = new HashMap<>();
// 空值處理
if (StringUtils.isBlank(username)) {
map.put("usernameMsg", "賬號(hào)不能為空!");
return map;
}
if (StringUtils.isBlank(password)) {
map.put("passwordMsg", "密碼不能為空!");
return map;
}
// 驗(yàn)證賬號(hào)
User user = userMapper.selectByName(username);
if (user == null) {
map.put("usernameMsg", "該賬號(hào)不存在!");
return map;
}
// 驗(yàn)證狀態(tài)
if (user.getStatus() == 0) {
map.put("usernameMsg", "該賬號(hào)未激活!");
return map;
}
// 驗(yàn)證密碼
password = CommunityUtil.md5(password + user.getSalt());
if (!user.getPassword().equals(password)) {
map.put("passwordMsg", "密碼不正確!");
return map;
}
// 生成登錄憑證
LoginTicket loginTicket = new LoginTicket();
loginTicket.setUserId(user.getId());
loginTicket.setTicket(CommunityUtil.generateUUID());
loginTicket.setStatus(0);
loginTicket.setExpired(new Date(System.currentTimeMillis() + expiredSeconds * 1000));
loginTicketMapper.insertLoginTicket(loginTicket);
map.put("ticket", loginTicket.getTicket());
return map;
}
public void logout(String ticket) {
loginTicketMapper.updateStatus(ticket, 1);
}
2、LoginController
- 驗(yàn)證賬號(hào)、密碼、驗(yàn)證碼。
- 成功時(shí),生成登錄憑證,發(fā)放給客戶端。
- 失敗時(shí),跳轉(zhuǎn)回登錄頁。
@RequestMapping(path = "/login", method = RequestMethod.POST)
public String login(String username, String password, String code, boolean rememberme,
Model model, HttpSession session, HttpServletResponse response) {
// 檢查驗(yàn)證碼
String kaptcha = (String) session.getAttribute("kaptcha");
if (StringUtils.isBlank(kaptcha) || StringUtils.isBlank(code) || !kaptcha.equalsIgnoreCase(code)) {
model.addAttribute("codeMsg", "驗(yàn)證碼不正確!");
return "/site/login";
}
// 檢查賬號(hào),密碼
int expiredSeconds = rememberme ? REMEMBER_EXPIRED_SECONDS : DEFAULT_EXPIRED_SECONDS;
Map<String, Object> map = userService.login(username, password, expiredSeconds);
if (map.containsKey("ticket")) {
Cookie cookie = new Cookie("ticket", map.get("ticket").toString());
cookie.setPath(contextPath); // cookie路徑——整個(gè)項(xiàng)目
cookie.setMaxAge(expiredSeconds); // cookie 有效時(shí)間
response.addCookie(cookie); // 把 cookie 發(fā)送給頁面上
return "redirect:/index";
} else {
model.addAttribute("usernameMsg", map.get("usernameMsg"));
model.addAttribute("passwordMsg", map.get("passwordMsg"));
return "/site/login";
}
}
3、登錄頁面
發(fā)送請(qǐng)求時(shí),修改表單提交方式、路徑、名字
錯(cuò)誤信息展示,給默認(rèn)值
- 賬號(hào)
- 密碼
賬號(hào)相關(guān)的提示是動(dòng)態(tài)的
退出
- 將登錄憑證修改為失效狀態(tài)。
- 跳轉(zhuǎn)至網(wǎng)站首頁。
1、狀態(tài)標(biāo)識(shí)——UserService
- 將
ticket
改為 1,無效
public void logout(String ticket) {
loginTicketMapper.updateStatus(ticket, 1);
}
2、返回退出頁面請(qǐng)求——LoginController
- 返回重新登錄頁面
@RequestMapping(path = "/logout", method = RequestMethod.GET)
public String logout(@CookieValue("ticket") String ticket) {
userService.logout(ticket);
return "redirect:/login";
}
3、配置退出登錄頁面的鏈接——index
六、顯示登錄信息
攔截器
- 示例定義攔截器,實(shí)現(xiàn)
Handlerlnterceptor
- 配置攔截器,為它指定攔截、排除的路徑
攔截器應(yīng)用
- 在請(qǐng)求開始時(shí)查詢登錄用戶。
- 在本次請(qǐng)求中持有用戶數(shù)據(jù)
- 在模板視圖上顯示用戶數(shù)據(jù)
- 在請(qǐng)求結(jié)束時(shí)清理用戶數(shù)據(jù)
攔截器可以攔截請(qǐng)求,在攔截請(qǐng)求的開始和結(jié)束,插入一些代碼
1、攔截器
- 示例定義攔截器,實(shí)現(xiàn)
Handlerlnterceptor
- 配置攔截器,為它指定攔截、排除的路徑
攔截器測(cè)試——AlphaInterceptor
package com.nowcoder.community.controller.interceptor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@Component
public class AlphaInterceptor implements HandlerInterceptor {
// 日志——debug級(jí)別
private static final Logger logger = LoggerFactory.getLogger(AlphaInterceptor.class);
// 在Controller之前執(zhí)行, 請(qǐng)求之前執(zhí)行
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
logger.debug("preHandle: " + handler.toString()); //debug級(jí)別
return true;
}
// 在Controller之后執(zhí)行, 模板之前執(zhí)行
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
logger.debug("postHandle: " + handler.toString());
}
// 在 TemplateEngine 之后執(zhí)行
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
logger.debug("afterCompletion: " + handler.toString());
}
}
配置類——WebMvcConfig
- 實(shí)現(xiàn)接口——
WebMvcConfigurer
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
//攔截器注入
@Autowired
private AlphaInterceptor alphaInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
/*
攔截一切請(qǐng)求,不攔截的 加后邊
*/
// /**/*.css _ static 目錄下
registry.addInterceptor(alphaInterceptor)
.excludePathPatterns("/**/*.css", "/**/*.js", "/**/*.png", "/**/*.jpg", "/**/*.jpeg")
.addPathPatterns("/register", "/login"); // 攔截注冊(cè) 和登錄
}
2、攔截器應(yīng)用
- 在請(qǐng)求開始時(shí)查詢登錄用戶。
- 在本次請(qǐng)求中持有用戶數(shù)據(jù)
- 在模板視圖上顯示用戶數(shù)據(jù)
- 在請(qǐng)求結(jié)束時(shí)清理用戶數(shù)據(jù)
攔截器——LoginTicketInterceptor
每次請(qǐng)求的過程
request獲取Cookie——CookieUtil
- 復(fù)用request獲取Cookie
- 返回Cookie中的值
public class CookieUtil {
public static String getValue(HttpServletRequest request, String name) {
if (request == null || name == null) {
throw new IllegalArgumentException("參數(shù)為空!");
}
Cookie[] cookies = request.getCookies();
if (cookies != null) {
for (Cookie cookie : cookies) {
// cookie 的 name 是不是傳入的
if (cookie.getName().equals(name)) {
return cookie.getValue();
}
}
}
return null;
}
}
查詢登錄憑證——UserService
//查詢登錄憑證
public LoginTicket findLoginTicket(String ticket) {
return loginTicketMapper.selectByTicket(ticket);
}
找map——HostHolder
- 持有用戶信息,用于代替session對(duì)象.
/**
* 持有用戶信息,用于代替session對(duì)象.
*/
@Component
public class HostHolder {
private ThreadLocal<User> users = new ThreadLocal<>();
public void setUser(User user) {
users.set(user);
}
public User getUser() {
return users.get();
}
public void clear() {
users.remove();
}
}
攔截器主體代碼-LoginTicketInterceptor
@Component
public class LoginTicketInterceptor implements HandlerInterceptor {
@Autowired
private UserService userService;
@Autowired
private HostHolder hostHolder;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 從 cookie 中獲取憑證 cookie——ticket
String ticket = CookieUtil.getValue(request, "ticket");
if (ticket != null) {
// 查詢憑證
LoginTicket loginTicket = userService.findLoginTicket(ticket);
// 檢查憑證是否有效
// 憑證不為空、狀態(tài)為 0、超時(shí)時(shí)間晚于登陸時(shí)間
if (loginTicket != null && loginTicket.getStatus() == 0 && loginTicket.getExpired().after(new Date())) {
// 根據(jù)憑證查詢用戶
User user = userService.findUserById(loginTicket.getUserId());
// 在本次請(qǐng)求中持有用戶
hostHolder.setUser(user);
}
}
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
User user = hostHolder.getUser();
if (user != null && modelAndView != null) {
// 將 user 添加到 model
modelAndView.addObject("loginUser", user);
}
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
// 清理數(shù)據(jù)
hostHolder.clear();
}
}
配置——WebMvcConfig
@Autowired
private LoginTicketInterceptor loginTicketInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
/*
攔截一切請(qǐng)求,不攔截的 加后邊
*/
registry.addInterceptor(loginTicketInterceptor)
.excludePathPatterns("/**/*.css", "/**/*.js", "/**/*.png", "/**/*.jpg", "/**/*.jpeg");
}
首頁——index
登錄 才能看到 消息
沒登錄 才顯示 注冊(cè)
沒登錄 才顯示 登錄
調(diào)整登錄賬號(hào)顯示
3、運(yùn)行結(jié)果:
4、總結(jié)
關(guān)于Spring MVC攔截器:
- 攔截器需實(shí)現(xiàn)
HandlerInterceptor
接口,而WebMvcConfigurer
接口是MVC
配置類要實(shí)現(xiàn)的接口 -
preHandle
方法在Controller
之前執(zhí)行,若返回false
,則終止執(zhí)行后續(xù)的請(qǐng)求。 -
postHandle
方法在Controller
之后、模板之前執(zhí)行。 -
afterCompletion
方法在模板之后執(zhí)行。
關(guān)于配置Spring MVC攔截器
- 配置類需實(shí)現(xiàn)
WebMvcConfigurer
接口 - 通過
addInterceptors
方法對(duì)攔截器進(jìn)行配置 - 可以配置忽略攔截的路徑,也可以配置希望攔截的路徑
關(guān)于ThreadLocal的描述
-
ThreadLocal
采用線程隔離的方式存放數(shù)據(jù),可以避免多線程之間出現(xiàn)數(shù)據(jù)訪問沖突。 -
ThreadLocal
提供set
方法,能夠以當(dāng)前線程為key
存放數(shù)據(jù)。 -
ThreadLocal
提供get
方法,能夠以當(dāng)前線程為key
獲取數(shù)據(jù)。
七、賬號(hào)設(shè)置——上傳頭像、修改密碼
上傳文件
- 請(qǐng)求:必須是
POST
請(qǐng)求 - 表單:
enctype="multipart/form-data'
-
Spring MVC
:通過MultipartFile處理上傳文件
開發(fā)步驟
- 訪問賬號(hào)設(shè)置頁面
- 上傳頭像
- 獲取頭像
完成這個(gè)頁面
1、可以訪問這個(gè)頁面
返回訪問頁面——UserController
@RequestMapping(path = "/setting", method = RequestMethod.GET)
public String getSettingPage() {
return "/site/setting";
}
顯示頁面——setting.html
修改路徑等
index中修改——鏈接
2、上傳頭像
- 開放時(shí),是
Windows
- 上線時(shí),是
Linux
配置文件
添加 存儲(chǔ) 上傳文件的路徑
community.path.upload=j:/work/data/upload
UserService
更新修改圖像的路徑,返回更新行數(shù)
public int updateHeader(int userId, String headerUrl) {
return userMapper.updateHeader(userId, headerUrl);
}
UserController
- 上傳表單提交為
post
請(qǐng)求 - 項(xiàng)目域名
- 項(xiàng)目名
private static final Logger logger = LoggerFactory.getLogger(UserController.class);
@Value("${community.path.upload}")
private String uploadPath; // 上傳路徑
@Value("${community.path.domain}")
private String domain; // 域名
@Value("${server.servlet.context-path}")
private String contextPath; //項(xiàng)目名
@Autowired
private UserService userService;
@Autowired
private HostHolder hostHolder; //取 當(dāng)前用戶是誰
@RequestMapping(path = "/upload", method = RequestMethod.POST)
public String uploadHeader(MultipartFile headerImage, Model model) {
if (headerImage == null) {
model.addAttribute("error", "您還沒有選擇圖片!");
return "/site/setting";
}
String fileName = headerImage.getOriginalFilename(); // 讀取文件的后綴
String suffix = fileName.substring(fileName.lastIndexOf(".")); // 截取后綴
if (StringUtils.isBlank(suffix)) {
model.addAttribute("error", "文件的格式不正確!");
return "/site/setting";
}
// 生成隨機(jī)文件名
fileName = CommunityUtil.generateUUID() + suffix;
// 確定文件存放的路徑
File dest = new File(uploadPath + "/" + fileName);
try {
// 存儲(chǔ)文件
headerImage.transferTo(dest);
} catch (IOException e) {
logger.error("上傳文件失敗: " + e.getMessage());
throw new RuntimeException("上傳文件失敗,服務(wù)器發(fā)生異常!", e);
}
// 更新當(dāng)前用戶的頭像的路徑(web訪問路徑)
// http://localhost:8080/community/user/header/xxx.png
User user = hostHolder.getUser();
String headerUrl = domain + contextPath + "/user/header/" + fileName;
userService.updateHeader(user.getId(), headerUrl);
return "redirect:/index";
}
@RequestMapping(path = "/header/{fileName}", method = RequestMethod.GET)
public void getHeader(@PathVariable("fileName") String fileName, HttpServletResponse response) {
// 服務(wù)器存放路徑
fileName = uploadPath + "/" + fileName;
// 文件后綴
String suffix = fileName.substring(fileName.lastIndexOf("."));
// 響應(yīng)圖片
response.setContentType("image/" + suffix);
try (
FileInputStream fis = new FileInputStream(fileName);
OutputStream os = response.getOutputStream();
) {
byte[] buffer = new byte[1024];
int b = 0;
while ((b = fis.read(buffer)) != -1) {
os.write(buffer, 0, b);
}
} catch (IOException e) {
logger.error("讀取頭像失敗: " + e.getMessage());
}
}
賬號(hào)設(shè)置——setting.html
3、修改密碼
UserService
public User findUserByName(String username) {
return userMapper.selectByName(username);
}
// 重置密碼
public Map<String, Object> resetPassword(String email, String password) {
Map<String, Object> map = new HashMap<>();
// 空值處理
if (StringUtils.isBlank(email)) {
map.put("emailMsg", "郵箱不能為空!");
return map;
}
if (StringUtils.isBlank(password)) {
map.put("passwordMsg", "密碼不能為空!");
return map;
}
// 驗(yàn)證郵箱
User user = userMapper.selectByEmail(email);
if (user == null) {
map.put("emailMsg", "該郵箱尚未注冊(cè)!");
return map;
}
// 重置密碼
password = CommunityUtil.md5(password + user.getSalt());
userMapper.updatePassword(user.getId(), password);
map.put("user", user);
return map;
}
// 修改密碼
public Map<String, Object> updatePassword(int userId, String oldPassword, String newPassword) {
Map<String, Object> map = new HashMap<>();
// 空值處理
if (StringUtils.isBlank(oldPassword)) {
map.put("oldPasswordMsg", "原密碼不能為空!");
return map;
}
if (StringUtils.isBlank(newPassword)) {
map.put("newPasswordMsg", "新密碼不能為空!");
return map;
}
// 驗(yàn)證原始密碼
User user = userMapper.selectById(userId);
oldPassword = CommunityUtil.md5(oldPassword + user.getSalt());
if (!user.getPassword().equals(oldPassword)) {
map.put("oldPasswordMsg", "原密碼輸入有誤!");
return map;
}
// 更新密碼
newPassword = CommunityUtil.md5(newPassword + user.getSalt());
userMapper.updatePassword(userId, newPassword);
return map;
}
UserController
// 修改密碼
@RequestMapping(path = "/updatePassword", method = RequestMethod.POST)
public String updatePassword(String oldPassword, String newPassword, Model model) {
User user = hostHolder.getUser();
Map<String, Object> map = userService.updatePassword(user.getId(), oldPassword, newPassword);
if (map == null || map.isEmpty()) {
return "redirect:/logout";
} else {
model.addAttribute("oldPasswordMsg", map.get("oldPasswordMsg"));
model.addAttribute("newPasswordMsg", map.get("newPasswordMsg"));
return "/site/setting";
}
}
setting.html
更改
總結(jié)
上傳文件的必要條件
- 必須在POST請(qǐng)求中上傳文件
- 表單的enctype屬性必須設(shè)置為“multipart/form-data”
關(guān)于上傳路徑與訪問路徑的描述
- 上傳路徑可以是本地路徑也可以是web路徑,
- 訪問路徑必須是符合HTTP協(xié)議的Web路徑。
關(guān)于MultipartFile
類型的描述
- 一個(gè)
MultipartFile
只能封裝一個(gè)文件 - 通過
MultipartFile
的getOriginalFilename
方法,可以獲得原始文件名 - 通過
MultipartFile
的transferTo
方法,可以將文件存入指定位置
八、檢查登錄狀態(tài)
- 沒有登陸也能訪問登錄后的頁面
- 安全隱患
- 攔截器——不在配置文件中攔截,用注解在方法上攔截
使用攔截器
- 在方法前標(biāo)注自定義注解
- 攔截所有請(qǐng)求,只處理帶有該注解的方法
自定義注解
常用的元注解:
-
@Target
、——聲明自定義注解作用在哪個(gè)位置,例如方法上、類上 -
@Retention
、——聲明自定義注解的有效時(shí)間(編譯時(shí)、運(yùn)行時(shí)) -
@Document
、——聲明自定義注解生成文檔的時(shí)候要不要把注解帶上去 -
@Inherited
——用于繼承,父類有注解,子類是否要繼承
如何讀取注解:
- 反射
Method.getDeclaredAnnotations()
Method.getAnnotation(class<T>annotationclass)
1、寫注解——LoginRequired
新建一個(gè)包,寫注解
@Target(ElementType.METHOD) // 方法上
@Retention(RetentionPolicy.RUNTIME) //程序運(yùn)行時(shí)有效
public @interface LoginRequired {
// 里邊不用寫內(nèi)容,標(biāo)注解就行
}
2、加上注解——UserController
在需要的方法上,加上注解
3、攔截器——LoginRequiredInterceptor
@Component
public class LoginRequiredInterceptor implements HandlerInterceptor {
@Autowired
private HostHolder hostHolder; // 獲取當(dāng)前用戶
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 判斷攔截到的是不是方法
if (handler instanceof HandlerMethod) {
HandlerMethod handlerMethod = (HandlerMethod) handler; // 轉(zhuǎn)型
Method method = handlerMethod.getMethod(); // 獲得方法
LoginRequired loginRequired = method.getAnnotation(LoginRequired.class); // 從方法里取注解
// 當(dāng)前方法需要登錄,但是用戶沒登錄
if (loginRequired != null && hostHolder.getUser() == null) {
// 重定向 —— 項(xiàng)目名
response.sendRedirect(request.getContextPath() + "/login");
return false; // 拒絕請(qǐng)求
}
}
return true;
}
}
4、攔截器配置——WebMvcConfig
攔截器配置——指定生成的路徑
好處,攔截誰,就給誰加注解
// 攔截指定方法
@Autowired
private LoginRequiredInterceptor loginRequiredInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
/*
攔截一切請(qǐng)求,不攔截的 加后邊
*/
// /**/*.css _ static 目錄下
registry.addInterceptor(loginRequiredInterceptor)
.excludePathPatterns("/**/*.css", "/**/*.js", "/**/*.png", "/**/*.jpg", "/**/*.jpeg");
}
總結(jié)
關(guān)于元注解
- @Target用于描述該注解可以作用的目標(biāo)類型
- @Retention用于描述該注解被保留的時(shí)間
- @Document用于描述該注解是否可以生成到文檔里
- 比如LoginRequired加上了這個(gè)@Inherited,那注解LoginRequired的類的子類也會(huì)自動(dòng)注解上LoginRequired
關(guān)于解析注解文章來源:http://www.zghlxwxcb.cn/news/detail-462018.html
- 在程序中,可以通過反射的方式解析注解
- 通過Method對(duì)象可以獲取某方法上標(biāo)注的所有注解
- 通過Method對(duì)象可以獲取某方法上指定類型的注解
- Method對(duì)象上還有很多其他的方法,可以獲取該方法上標(biāo)注的注解
在程序中,可以通過哪些方式正確實(shí)現(xiàn)重定向文章來源地址http://www.zghlxwxcb.cn/news/detail-462018.html
- 在Controller的方法里,通過返回以”redirect”開頭的字符串實(shí)現(xiàn)重定向
- 在Controller的方法里,通過response對(duì)象的sendRedirect方法實(shí)現(xiàn)重定向
- 在攔截器中,通過response對(duì)象的sendRedirect方法實(shí)現(xiàn)重定向
到了這里,關(guān)于【論壇java項(xiàng)目】第二章 Spring Boot實(shí)踐,開發(fā)社區(qū)登錄模塊:發(fā)送郵件、開發(fā)注冊(cè)功能、會(huì)話管理、生成驗(yàn)證碼、開發(fā)登錄、退出功能、的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!