1 分布式session

1.1 用户登录功能实现 分布式Session 异常处理器 根据token获取用户信息

1 .1.1 使用两次MD5

1
2
3
4
5
1.用户端:PASS = MD5(明文+固定Salt) 防止用户明文密码在网络中传输

2.服务端:PASS = MD5(用户输入+随机Salt) 防止被脱裤

引入MD5工具类,添加MD5Util
1
2
3
4
5
6
7
8
9
10
11
<!--MD5-->
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
</dependency>

<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.6</version>
</dependency>

1 .1.2 MD5Util

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
package com.imooc.miaosha.util;

import org.apache.commons.codec.digest.DigestUtils;

public class MD5Util {

public static String md5(String src) {
return DigestUtils.md5Hex(src);
}

private static final String salt = "1a2b3c4d";

public static String inputPassToFormPass(String inputPass) {
String str = ""+salt.charAt(0)+salt.charAt(2) + inputPass +salt.charAt(5) + salt.charAt(4);
System.out.println(str);
return md5(str);
}

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 saltDB) {
String formPass = inputPassToFormPass(inputPass);
String dbPass = formPassToDBPass(formPass, saltDB);
return dbPass;
}

public static void main(String[] args) {
System.out.println(inputPassToFormPass("123456"));//d3b1294a61a07da9b49b6e22b2cbd7f9
// System.out.println(formPassToDBPass(inputPassToFormPass("123456"), "1a2b3c4d"));
// System.out.println(inputPassToDbPass("123456", "1a2b3c4d"));//b7797cce01b4b131b433b6acf4add449
}

}

1.1.3 使用 JSR303参数校验

1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>

先在参数中加上注解

1
doLogin(@Valid LoginVo loginVo)

在实体类中校验

1
2
3
4
5
6
@NotNull
private String mobile;

@NotNull
@Length(min=32)
private String password;

1.1.4 如何自定义一个验证器,比如检验手机号格式

先定义校验注解

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
package com.imooc.miaosha.validator;

import static java.lang.annotation.ElementType.ANNOTATION_TYPE;
import static java.lang.annotation.ElementType.CONSTRUCTOR;
import static java.lang.annotation.ElementType.FIELD;
import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.ElementType.PARAMETER;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;

import javax.validation.Constraint;
import javax.validation.Payload;

@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER })
@Retention(RUNTIME)
@Documented
@Constraint(validatedBy = {IsMobileValidator.class })
public @interface IsMobile {

boolean required() default true;

String message() default "手机号码格式错误";

Class<?>[] groups() default { };

Class<? extends Payload>[] payload() default { };
}

1.1.5 再定义一个真正校验的

IsMobileValidator.class

要实现 ConstraintValidator 接口

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
package com.imooc.miaosha.validator;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;

import org.apache.commons.lang3.StringUtils;

import com.imooc.miaosha.util.ValidatorUtil;

public class IsMobileValidator implements ConstraintValidator<IsMobile, String> {

private boolean required = false;

public void initialize(IsMobile constraintAnnotation) {
required = constraintAnnotation.required();
}

public boolean isValid(String value, ConstraintValidatorContext context) {
if(required) {
return ValidatorUtil.isMobile(value);
}else {
if(StringUtils.isEmpty(value)) {
return true;
}else {
return ValidatorUtil.isMobile(value);
}
}
}

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package com.imooc.miaosha.util;

import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.apache.commons.lang3.StringUtils;

public class ValidatorUtil {

private static final Pattern mobile_pattern = Pattern.compile("1\\d{10}");

public static boolean isMobile(String src) {
if(StringUtils.isEmpty(src)) {
return false;
}
Matcher m = mobile_pattern.matcher(src);
return m.matches();
}

// public static void main(String[] args) {
// System.out.println(isMobile("18912341234"));
// System.out.println(isMobile("1891234123"));
// }
}

现在这样会有异常,定义一个全局异常拦截

拦截所有异常,如果是绑定异常就返回对应的错误
如果是其他异常就返回服务端错误

2.1.1 异常处理器

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
package com.imooc.miaosha.exception;

import java.util.List;

import javax.servlet.http.HttpServletRequest;

import org.springframework.validation.BindException;
import org.springframework.validation.ObjectError;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;

import com.imooc.miaosha.result.CodeMsg;
import com.imooc.miaosha.result.Result;

@ControllerAdvice
@ResponseBody
public class GlobalExceptionHandler {
@ExceptionHandler(value=Exception.class)
public Result<String> exceptionHandler(HttpServletRequest request, Exception e){
e.printStackTrace();
if(e instanceof GlobalException) {
GlobalException ex = (GlobalException)e;
return Result.error(ex.getCm());
}else if(e instanceof BindException) {
BindException ex = (BindException)e;
List<ObjectError> errors = ex.getAllErrors();
ObjectError error = errors.get(0);
String msg = error.getDefaultMessage();
return Result.error(CodeMsg.BIND_ERROR.fillArgs(msg));
}else {
return Result.error(CodeMsg.SERVER_ERROR);
}
}
}

之后我们就可以抛出异常然后让其捕获就可以了

我们创建一个全局异常,然后抛出,可以在上面捕获在进行指定输出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package com.imooc.miaosha.exception;

import com.imooc.miaosha.result.CodeMsg;

public class GlobalException extends RuntimeException{

private static final long serialVersionUID = 1L;

private CodeMsg cm;

public GlobalException(CodeMsg cm) {
super(cm.toString());
this.cm = cm;
}

public CodeMsg getCm() {
return cm;
}

}

3 分布式Session

登录成功 给这个用户生成一个UUID,也就是token,传递给客户端。
客户端每次都上传这个 token就能获取到用户信息了。

这个token对应哪个 用户信息存到 redis中

将信息存到第三方缓存中

cookie增加成功,添加成功 token


image-20190924113350361

在登录完成的最后一步,需要带着Session信息。

1.利用uuid生成秘钥(sessionId)

2.将user信息,对象。同时写入cookie cookie作为response返回给客户端, 另外 将sessionId +前缀 一起作为Key,存入Redis 缓存中。当访问其他页面的时候,就可以从cookie中获取 token,再访问redis 拿到用户信息来判断登录情况了。


MiaoshaUserKey类:

1
2
3
4
5
6
7
8
9
10
11
12
13
public class MiaoshaUserKey extends BasePrefix {

/**
* 设置cookie过期时间
*/
public static final int TOKEN_EXPIRE = 3600*24 * 2;

private MiaoshaUserKey(int expireSeconds, String prefix) {
super(expireSeconds, prefix);
}

public static MiaoshaUserKey token = new MiaoshaUserKey(TOKEN_EXPIRE, "tk");
}

WebConfig类

1
2
3
4
5
6
7
8
9
10
11
@Configuration
public class WebConfig extends WebMvcConfigurerAdapter{

@Autowired
UserArgumentResolver userArgumentResolver;

@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
argumentResolvers.add(userArgumentResolver);
}
}

UserArgumentResolver类:

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
@Service
public class UserArgumentResolver implements HandlerMethodArgumentResolver {

@Autowired
MiaoshaUserService userService;

@Override
public boolean supportsParameter(MethodParameter parameter) {
Class<?> clazz = parameter.getParameterType();
return clazz== MiaoshaUser.class;
}

@Override
public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
HttpServletRequest request = webRequest.getNativeRequest(HttpServletRequest.class);
HttpServletResponse response = webRequest.getNativeResponse(HttpServletResponse.class);

String paramToken = request.getParameter(MiaoshaUserService.COOKIE_NAME_TOKEN);
String cookieToken = getCookieValue(request, MiaoshaUserService.COOKIE_NAME_TOKEN);
if(StringUtils.isEmpty(cookieToken) && StringUtils.isEmpty(paramToken)) {
return null;
}
String token = StringUtils.isEmpty(paramToken)?cookieToken:paramToken;
return userService.getByToken(response, token);
}

private String getCookieValue(HttpServletRequest request, String cookiName) {
Cookie[] cookies = request.getCookies();
for(Cookie cookie : cookies) {
if(cookie.getName().equals(cookiName)) {
return cookie.getValue();
}
}
return null;
}
}

Service层

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
public MiaoshaUser getByToken(HttpServletResponse response,String token) {

if(StringUtils.isEmpty(token)) {
return null;
}

MiaoshaUser user = redisService.get(MiaoshaUserKey.token, token, MiaoshaUser.class);
// 延长有效期
if(user != null) {
addCookie(response,user);
}

return user;
}

private void addCookie(HttpServletResponse response,MiaoshaUser miaoshaUser) {
// 生成cookie
String token = UUIDUtil.uuid();
/**
* MiaoshaUserKey.token: 前缀
* token: 标记
* value: 用户信息
* 拿到token就可以知道用户信息
*/
redisService.set(MiaoshaUserKey.token,token,miaoshaUser);
Cookie cookie = new Cookie(COOKIE_NAME_TOKEN,token);
cookie.setMaxAge(MiaoshaUserKey.token.expireSeconds());
cookie.setPath("/");
//cookie写入到客户端里
response.addCookie(cookie);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@RequestMapping("/to_list")
public String toLogin(Model model,
@CookieValue(value = MiaoshaUserService.COOKI_NAME_TOKEN,required = false)String cookieToken,
@RequestParam(value = MiaoshaUserService.COOKI_NAME_TOKEN,required = false)String paramToken,
HttpServletResponse response){
if (StringUtils.isEmpty(cookieToken)&&StringUtils.isEmpty(paramToken)){
return "do_login";
}
//优先取cookie中的token,两种都取是因为可能手机端是发的参数而不是cookie中。required = false 表示可以传值为空
String token = StringUtils.isEmpty(paramToken)?cookieToken:paramToken;
MiaoshaUser user =userService.getByToken(response,token);

model.addAttribute("user",user);

return "to_list";
}
1
2
3
4
5
6
7
8
9
private void addCookie(HttpServletResponse response, String token, MiaoshaUser user) {
//存到redis中,key是 tk:token值,值是序列化的用户
redisService.set(MiaoshaUserKey.token, token, user);
Cookie cookie = new Cookie(COOKI_NAME_TOKEN, token);
//设置cookie过期时间为toeken过期时间
cookie.setMaxAge(MiaoshaUserKey.token.expireSeconds());
cookie.setPath("/");
response.addCookie(cookie);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public boolean login(HttpServletResponse response, LoginVo loginVo) {
if(loginVo == null) {
throw new GlobleException(CodeMsg.SERVER_ERROR);
}
String mobile = loginVo.getMobile();
String formPass = loginVo.getPassword();
//判断手机号是否存在
MiaoshaUser user = getByMobile(mobile);
if(user == null) {
throw new GlobleException(CodeMsg.MOBILE_NOT_EXIST);
}
//验证密码
String dbPass = user.getPassword();
String saltDB = user.getSalt();
String calcPass = MD5Util.formPassToDBPass(formPass, saltDB);
if(!calcPass.equals(dbPass)) {
throw new GlobleException(CodeMsg.PASSWORD_ERROR);
}
//生成cookie
String token = UUIDUtil.uuid();
//随机生成 token加入到cookie中
addCookie(response, token, user);
return true;
}

spring-boot整合spring-session,使用redis共享

https://www.jianshu.com/p/cdf327a6a5a4

spring-boot整合spring-session,使用redis共享

12018.05.13 00:50:57字数 703阅读 10688

本文讲述spring-boot工程中使用spring-session机制进行安全认证,并且通过redis存储session,满足集群部署、分布式系统的session共享。

java工程中,说到权限管理和安全认证,我们首先想到的是Spring Security和Apache Shiro,这两者均能实现用户身份认证和复杂的权限管理功能。但是如果我们只是想实现身份认证(如是否登录、会话是否超时),使用session管理即可满足。本文目录如下:

目录:

  1. 创建spring-boot项目

  2. 用户管理

  3. 用户身份认证

  4. spring-session配置

  5. 使用redis共享session


一、创建spring-boot项目

1、工程使用idea+gradle搭建,jdk1.8,spring-boot版本2.0.2.RELEASE,数据库postgreSQL,持久层spring-data-jpa;
2、新建spring-boot项目,工程type选择Gradle Project;
3、勾选初始化依赖如下:

初始化依赖

初始化依赖

创建完成后gradle.build文件内容如下:

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
buildscript {
ext {
springBootVersion = '2.0.2.RELEASE'
}
repositories {
mavenCentral()
}
dependencies {
classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
}
}

apply plugin: 'java'
apply plugin: 'idea'
apply plugin: 'org.springframework.boot'
apply plugin: 'io.spring.dependency-management'

group = 'louie.share'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = 1.8
targetCompatibility = 1.8

repositories {
mavenCentral()
}


dependencies {
compile('org.springframework.boot:spring-boot-starter-data-jpa')
compile('org.springframework.boot:spring-boot-starter-web')
runtime('org.springframework.boot:spring-boot-devtools')
runtime('org.postgresql:postgresql')
testCompile('org.springframework.boot:spring-boot-starter-test')
}

4、application.yml配置数据库及jpa

1
2
3
4
5
6
7
8
9
10
spring:
datasource:
driver-class-name: org.postgresql.Driver
url: jdbc:postgresql://127.0.0.1:5432/louie
data-username: louie
password: louie1234
jpa:
database: postgresql
hibernate:
ddl-auto: update

二、用户管理

1、创建User实体类

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
import org.hibernate.annotations.GenericGenerator;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.Table;
import java.io.Serializable;

/**
* 用户实体
* @author louie
* @date created in 2018-5-12 17:28
*/
@Entity
@Table(name = "b_id_user")
public class User implements Serializable{
@Id
@GenericGenerator(name = "idGenerator",strategy = "uuid")
@GeneratedValue(generator = "idGenerator")
private String id;

@NotBlank(message = "account can not be empty")
private String account;
@NotBlank(message = "password can not be empty")
private String password;
@NotBlank(message = "name can not be empty")
private String name;

//省略getter、setter
}

2、用户服务接口

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
import louie.share.sessionredis.bean.User;

/**
* 用户服务接口
* @author louie
* @date created in 2018-5-12 17:40
*/
public interface UserService {
/**
* save user
* @param user
* @return 保存后的用户信息
*/
User saveUser(User user);

/**
* find user by account
* @param account
* @return
*/
User findByAccount(String account);

/**
* user login
* @param account
* @param password
* @return
*/
BaseResponse login(String account,String password);
}

这里省略接口的实现,您可以访问我的github和码云查看该工程的源代码(代码地址见文档底部)。

3、用户管理控制器

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
43
44
45
46
47
48
49
50
51
52
import louie.share.sessionredis.bean.BaseResponse;
import louie.share.sessionredis.bean.User;
import louie.share.sessionredis.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;

/**
* 用户管理相关控制器
* @author louie
* @date created in 2018-5-12 17:26
*/

@RestController
@RequestMapping(value = "/user")
public class UserController {
@Autowired
private UserService userService;

/**
* save user
* @param user
* @return
*/
@RequestMapping(value = "/save",method = RequestMethod.POST)
public User save(User user){
return userService.saveUser(user);
}

/**
* find user by account
* @param account
* @return
*/
@RequestMapping(value = "/find/{account}",method = RequestMethod.GET)
public User find(@PathVariable String account){
return userService.findByAccount(account);
}

/**
* user login
* @param account
* @param password
* @return
*/
@RequestMapping(value = "/login",method = RequestMethod.POST)
public BaseResponse login(String account,String password){
return userService.login(account,password);
}
}

4、创建用户
运行Application类启动服务,使用postMan访问http://localhost:8080/user/save服务创建用户:

创建用户

创建用户

三、用户身份认证

1、使用postMan访问http://localhost:8080/user/login进行用户登录校验:

微信截图_20180512184322.png-66.2kB

微信截图_20180512184322.png-66.2kB

四、spring-session配置

该部分为重点内容了,目的是实现访问资源时的安全认证、超时控制和用户登出功能。
1、修改用户登录login控制,登录成功后将用户信息写入session

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* user login
* @param account
* @param password
* @return
*/
@RequestMapping(value = "/login",method = RequestMethod.POST)
public BaseResponse login(String account, String password,HttpSession session){
BaseResponse response = userService.login(account,password);
if (response.isOk()){
session.setAttribute(session.getId(),response.getData());
}
return response;
}

2、新增用户登出logout功能,将用户信息移除session

1
2
3
4
5
6
7
8
9
10
/**
* user logout
* @param session
* @return
*/
@RequestMapping(value = "/logout")
public String logout(HttpSession session){
session.removeAttribute(session.getId());
return "user logout success";
}

3、设置session过期时间

1
2
3
4
5
6
7
8
9
10
11
12
13
14
spring:
datasource:
driver-class-name: org.postgresql.Driver
url: jdbc:postgresql://127.0.0.1:5432/louie
data-username: louie
password: louie1234
jpa:
database: postgresql
hibernate:
ddl-auto: update
server:
servlet:
session:
timeout: "PT10M"

以下是为session有效时长为10分钟:

设置session过期时间

设置session过期时间

4、添加拦截器,通过session判断用户是否有效

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
43
44
45
46
47
48
49
import com.alibaba.fastjson.JSON;
import louie.share.sessionredis.bean.BaseResponse;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.io.IOException;

/**
* @author louie
* @date created in 2018-5-12 19:02
*/
@Configuration
public class SessionCofig implements WebMvcConfigurer{

@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new SecurityInterceptor())
//排除拦截
.excludePathPatterns("/user/login")
.excludePathPatterns("/user/logout")

//拦截路径
.addPathPatterns("/**");
}


@Configuration
public class SecurityInterceptor implements HandlerInterceptor{
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws IOException {
HttpSession session = request.getSession();
if (session.getAttribute(session.getId()) != null){
return true;
}
response.getWriter().write(JSON.toJSONString(
new BaseResponse(){{
setOk(false);
setMessage("please login first");
}}
));
return false;
}
}
}

5、使用postMan访问http://localhost:8080/user/find/101,用户未登录,被拦截:

用户未登录拦截

用户未登录拦截

访问http://localhost:8080/user/login登录:

用户登录

用户登录

再次访问访问http://localhost:8080/user/find/101

登录后访问

登录后访问

五、使用redis存储session

1、添加依赖

1
2
compile('org.springframework.boot:spring-boot-starter-data-redis')
compile('org.springframework.session:spring-session-data-redis')

2、application.yml中添加配置

redis配置

redis配置

源代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
spring:
datasource:
driver-class-name: org.postgresql.Driver
url: jdbc:postgresql://127.0.0.1:5432/louie
data-username: louie
password: louie1234
jpa:
database: postgresql
hibernate:
ddl-auto: update
redis:
database: 0
host: localhost
port: 6379
password: xonro_vflow
session:
store-type: redis
server:
servlet:
session:
timeout: "PT10M"

3、启动redis和application类,用户登录,查看redis内容:

debug查看:

debug查看session信息

debug查看session信息

redis内容:

redis内容

redis内容

工程代码已共享至github和码云,欢迎探讨学习。