1. MD5加密(两次加密)
为了防止用户输入的明文密码在网络传输过程中被窃取,因此设置盐值加密,定义salt变量,通过加密规则对明文密码加密。第一次加密为:从用户端(前端)到后端,盐值设置为1a2b3c4d,加密规则为空字符串+盐值的第0位+第2位+明文密码+第五位+第四位。第二次加密为:从后端到数据库,盐值可从数据库表中字段调用,加密规则暂定与第一次相同。
在Maven中导入jar包
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
</dependency>
在utils包下创建MD5util.java
/**
* MD5工具类
* @ClassName: MD5Util
* 两次加密
* MD5(MD5(pass明文+固定salt) + salt)
*/
public class MD5Util {
public static String md5(String str) {
return DigestUtils.md5Hex(str);
}
private static final String salt = "1a2b3c4d";
/**
* 第一次加密
* @param inputPass
* @return java.lang.String
**/
public static String inputPassToFromPass(String inputPass) {
String str = "" + salt.charAt(0) + salt.charAt(2) + inputPass + salt.charAt(5) + salt.charAt(4);
return md5(str);
}
/**
* 第二次加密
* @param formPass
* @param salt
* @return java.lang.String
**/
public static String formPassToDBPass(String formPass, String salt) {
String str = "" +salt.charAt(0) + salt.charAt(2) + formPass + salt.charAt(5) + salt.charAt(4);
return md5(str);
}
public static String inputPassToDBPass(String inputPass, String salt) {
String fromPass = inputPassToFromPass(inputPass);
String dbPass = formPassToDBPass(fromPass, salt);
return dbPass;
}
// public static void main(String[] args) {
// String s = inputPassToDBPass("123456", "1a2b3c4d");
// String s1 = inputPassToFromPass("123456");
// String s2 = formPassToDBPass(s1, "1a2b3c4d");
// System.out.println(s);
// System.out.println(s2);
// }
}
2. 使用@Valid进行传入参数的验证
2.1使用注解验证
@Valid的详细用法请参考:@Valid用法
Spring Validation验证框架对参数的验证机制提供了@Validated,javax提供了@Valid,配合BindingResult可以直接提供参数验证结果,以检验Controller的入参是否符合规范,该方法可以避免代码的冗余,减少代码量——即重复的进行参数的验证
在Maven中导入jar包
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
在需要检验的参数处加入@Valid注解
@PostMapping("/doLogin")
@ResponseBody
//在需要检验的参数处加入@Valid注解
public RespBean doLogin(@Valid LoginVo loginVo, HttpServletRequest request, HttpServletResponse response) {
log.info("{}", loginVo);
return tUserService.doLogin(loginVo, request, response);
}
并在具体需要检验的地方加上具体的检验规则
@Data
public class LoginVo {
@NotNull
@IsMobile(required = true) // 自定义注解
private String mobile;
@NotNull
@Length(min = 32)
private String password;
}
2.2 自定义注解——验证参数格式
首先提供具体的验证模式:
这里我们对手机号进行验证,由于手机都有特定的格式因此我们在util下创建验证的类
/**
* 手机号码校验类
*/
public class ValidatorUtil {
//验证格式定义
private static final Pattern mobile_patten = Pattern.compile("[1]([3-9])[0-9]{9}$");
/**
* 手机号码校验
* @param mobile
* @return boolean
**/
public static boolean isMobile(String mobile) {
if (StringUtils.isEmpty(mobile)) {
return false;
}
Matcher matcher = mobile_patten.matcher(mobile);
return matcher.matches();
}
}
自定义注解IsMobile
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER, ElementType.TYPE_USE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Constraint(validatedBy = {IsMobileValidator.class}) //这里提供具体的验证方法
public @interface IsMobile {
boolean required() default true; // 手机号必填
String message() default "手机号格式错误";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
创建IsMobileValidator验证规则
public class IsMobileValidator implements ConstraintValidator<IsMobile, String> {
private boolean required = false; // 是否必填
@Override
public void initialize(IsMobile constraintAnnotation) {
required = constraintAnnotation.required();
}
@Override
public boolean isValid(String value, ConstraintValidatorContext constraintValidatorContext) {
if (required) // 必填时检验手机号格式
return ValidatorUtil.isMobile(value);
else if (StringUtils.isEmpty(value)) // 非必填,未填时直接放行
return true;
else
return ValidatorUtil.isMobile(value); // 非必填,填了需要检验
}
}
在使用了@Valid注解后,能够把参数绑定之后的校验结果给到BindingResult实例,出现校验错误时则会抛出异常,下节我们将通过定义全局的异常处理器捕获这些异常
3. 自定义异常并处理BindException
自定义异常
//这里的respBeanEnum主要封装了需要返回给前端的各类信息
@Data
@NoArgsConstructor
@AllArgsConstructor
public class GlobalException extends RuntimeException{
private RespBeanEnum respBeanEnum;
}
全局异常处理器——定义处理异常方法GlobalExceptionHandler
@RestControllerAdvice
//@ResponseBody
//指定返回类型为json格式,一般用于向html返回数据,表示该方法的返回结果直接写入 HTTP response body 中,一般在异步获取数据时使用【也就是AJAX】。
public class GlobalExceptionHandler {
@ExceptionHandler(Exception.class)
public RespBean ExceptionHandler(Exception e) {
if (e instanceof GlobalException) {
GlobalException ex = (GlobalException) e;
return RespBean.error(ex.getRespBeanEnum());
} else if (e instanceof BindException) {
BindException ex = (BindException) e;
RespBean respBean = RespBean.error(RespBeanEnum.BIND_ERROR);
respBean.setMessage("参数校验异常:" + ex.getBindingResult().getAllErrors().get(0).getDefaultMessage());
return respBean;
}
return RespBean.error(RespBeanEnum.ERROR);
}
}
使用自定义异常
//在需要的地方,RespBeanEnum是一个Enum
throws new GlobalException(RespBeanEnum.具体的元素)
4. session、cookie和token
4.1 设置uuid,cookie的配置类
(1)cookie工具类(utils下)
package com.example.seckilldemo.utils;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.net.URLEncoder;
/**
* Cookie工具类
* @ClassName: CookieUtil
*/
public final class CookieUtil {
/**
* 得到Cookie的值, 不编码
*
* @param request
* @param cookieName
* @return
*/
public static String getCookieValue(HttpServletRequest request, String cookieName) {
return getCookieValue(request, cookieName, false);
}
/**
* 得到Cookie的值,
*
* @param request
* @param cookieName
* @return
*/
public static String getCookieValue(HttpServletRequest request, String cookieName, boolean isDecoder) {
Cookie[] cookieList = request.getCookies();
if (cookieList == null || cookieName == null) {
return null;
}
String retValue = null;
try {
for (int i = 0; i < cookieList.length; i++) {
if (cookieList[i].getName().equals(cookieName)) {
if (isDecoder) {
retValue = URLDecoder.decode(cookieList[i].getValue(), "UTF-8");
} else {
retValue = cookieList[i].getValue();
}
break;
}
}
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
return retValue;
}
/**
* 得到Cookie的值,
*
* @param request
* @param cookieName
* @return
*/
public static String getCookieValue(HttpServletRequest request, String cookieName, String encodeString) {
Cookie[] cookieList = request.getCookies();
if (cookieList == null || cookieName == null) {
return null;
}
String retValue = null;
try {
for (int i = 0; i < cookieList.length; i++) {
if (cookieList[i].getName().equals(cookieName)) {
retValue = URLDecoder.decode(cookieList[i].getValue(), encodeString);
break;
}
}
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
return retValue;
}
/**
* 设置Cookie的值 不设置生效时间默认浏览器关闭即失效,也不编码
*/
public static void setCookie(HttpServletRequest request, HttpServletResponse response, String cookieName,
String cookieValue) {
setCookie(request, response, cookieName, cookieValue, -1);
}
/**
* 设置Cookie的值 在指定时间内生效,但不编码
*/
public static void setCookie(HttpServletRequest request, HttpServletResponse response, String cookieName,
String cookieValue, int cookieMaxage) {
setCookie(request, response, cookieName, cookieValue, cookieMaxage, false);
}
/**
* 设置Cookie的值 不设置生效时间,但编码
*/
public static void setCookie(HttpServletRequest request, HttpServletResponse response, String cookieName,
String cookieValue, boolean isEncode) {
setCookie(request, response, cookieName, cookieValue, -1, isEncode);
}
/**
* 设置Cookie的值 在指定时间内生效, 编码参数
*/
public static void setCookie(HttpServletRequest request, HttpServletResponse response, String cookieName,
String cookieValue, int cookieMaxage, boolean isEncode) {
doSetCookie(request, response, cookieName, cookieValue, cookieMaxage, isEncode);
}
/**
* 设置Cookie的值 在指定时间内生效, 编码参数(指定编码)
*/
public static void setCookie(HttpServletRequest request, HttpServletResponse response, String cookieName,
String cookieValue, int cookieMaxage, String encodeString) {
doSetCookie(request, response, cookieName, cookieValue, cookieMaxage, encodeString);
}
/**
* 删除Cookie带cookie域名
*/
public static void deleteCookie(HttpServletRequest request, HttpServletResponse response,
String cookieName) {
doSetCookie(request, response, cookieName, "", -1, false);
}
/**
* 设置Cookie的值,并使其在指定时间内生效
*
* @param cookieMaxage cookie生效的最大秒数
*/
private static final void doSetCookie(HttpServletRequest request, HttpServletResponse response,
String cookieName, String cookieValue, int cookieMaxage, boolean isEncode) {
try {
if (cookieValue == null) {
cookieValue = "";
} else if (isEncode) {
cookieValue = URLEncoder.encode(cookieValue, "utf-8");
}
Cookie cookie = new Cookie(cookieName, cookieValue);
if (cookieMaxage > 0)
cookie.setMaxAge(cookieMaxage);
if (null != request) {// 设置域名的cookie
String domainName = getDomainName(request);
System.out.println(domainName);
if (!"localhost".equals(domainName)) {
cookie.setDomain(domainName);
}
}
cookie.setPath("/");
response.addCookie(cookie);
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 设置Cookie的值,并使其在指定时间内生效
*
* @param cookieMaxage cookie生效的最大秒数
*/
private static final void doSetCookie(HttpServletRequest request, HttpServletResponse response,
String cookieName, String cookieValue, int cookieMaxage, String encodeString) {
try {
if (cookieValue == null) {
cookieValue = "";
} else {
cookieValue = URLEncoder.encode(cookieValue, encodeString);
}
Cookie cookie = new Cookie(cookieName, cookieValue);
if (cookieMaxage > 0) {
cookie.setMaxAge(cookieMaxage);
}
if (null != request) {// 设置域名的cookie
String domainName = getDomainName(request);
System.out.println(domainName);
if (!"localhost".equals(domainName)) {
cookie.setDomain(domainName);
}
}
cookie.setPath("/");
response.addCookie(cookie);
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 得到cookie的域名
*/
private static final String getDomainName(HttpServletRequest request) {
String domainName = null;
// 通过request对象获取访问的url地址
String serverName = request.getRequestURL().toString();
if (serverName == null || serverName.equals("")) {
domainName = "";
} else {
// 将url地下转换为小写
serverName = serverName.toLowerCase();
// 如果url地址是以http://开头 将http://截取
if (serverName.startsWith("http://")) {
serverName = serverName.substring(7);
}
int end = serverName.length();
// 判断url地址是否包含"/"
if (serverName.contains("/")) {
//得到第一个"/"出现的位置
end = serverName.indexOf("/");
}
// 截取
serverName = serverName.substring(0, end);
// 根据"."进行分割
final String[] domains = serverName.split("\\.");
int len = domains.length;
if (len > 3) {
// www.xxx.com.cn
domainName = domains[len - 3] + "." + domains[len - 2] + "." + domains[len - 1];
} else if (len <= 3 && len > 1) {
// xxx.com or xxx.cn
domainName = domains[len - 2] + "." + domains[len - 1];
} else {
domainName = serverName;
}
}
if (domainName != null && domainName.indexOf(":") > 0) {
String[] ary = domainName.split("\\:");
domainName = ary[0];
}
return domainName;
}
}
(2)uuid工具类(utils下)
/**
* UUID工具类
* @ClassName: UUIDUtil
*/
public class UUIDUtil {
public static String uuid() {
return UUID.randomUUID().toString().replace("-", "");
}
}
4.2 使用
以cookie,session判断用户是否登录成功
//在登陆时,生成cookie
String ticket = UUIDUtil.uuid();
request.getSession().setAttribute(ticket, user);
CookieUtil.setCookie(request, response, "userTicket", ticket);
登陆成功后跳转
//获取Cookie中的值,查session
@Controller
@RequestMapping("/goods")
public class GoodsController {
@RequestMapping("/toList") // userTicket 为自定义的cookie
public String toList(HttpSession session, Model model, @CookieValue("userTicket") String ticket) {
if (StringUtils.isEmpty(ticket))
return "login";
User user = (User) session.getAttribute(ticket);
if (user == null)
return "login";
model.addAttribute("user", user); // 将user传递到前端页面
return "goodsList";
}
}
5 分布式Session问题
5.1 存在的问题
-
之前的代码在我们之后一台应用系统,所有操作都在一台Tomcat上,没有什么问题。当我们部署多台
系统,配合Nginx的时候会出现用户登录的问题 -
但当Nginx 使用默认负载均衡策略(轮询),请求将会按照时间顺序逐一分发到后端应用上。也就是说刚开始我们在 Tomcat1 登录之后,用户信息放在 Tomcat1 的 Session 里。过了一会,请求又被 Nginx 分发到了 Tomcat2 上,这时 Tomcat2 上 Session 里还没有用户信息,于是又要登录。
5.2 解决方案
具体参鉴:session分布式解决方法
- Session复制
- 优点
- 无需修改代码,只需要修改Tomcat配置
- 缺点
- Session同步传输占用内网带宽
- 多台Tomcat同步性能指数级下降
- Session占用内存,无法有效水平扩展
- 优点
- 前端存储
- 优点
- 不占用服务端内存
- 缺点
- 存在安全风险
- 数据大小受cookie限制
- 占用外网带宽
- 优点
- Session粘滞
- 优点
- 无需修改代码
- 服务端可以水平扩展
- 缺点
1. 增加新机器,会重新Hash,导致重新登录
2. 应用重启,需要重新登录
- 优点
- 后端集中存储
- 优点
- 安全
- 容易水平扩展
- 缺点
- 增加复杂度
- 需要修改代码
- 优点