HTTP是无状态的协议,因此服务器无法确认用户的信息。W3C就提出了:给每一个用户都发一个通行证,无论谁访问的时候都需要携带通行证,这样服务器就可以从通行证上确认用户的信息。通行证就是Cookie。如果说Cookie是检查用户身上的”通行证“来确认用户的身份,那么Session就是通过检查服务器上的”客户明细表“来确认用户的身份的。Session相当于在服务器中建立了一份“客户明细表”。HTTP协议是无状态的,Session不能依据HTTP连接来判断是否为同一个用户。于是:服务器向用户浏览器发送了一个名为JESSIONID的Cookie,它的值是Session的id值。其实Session是依据Cookie来识别是否是同一个用户。
一、单机环境
在我们的项目中单机服务的会话管理是非常简单的,因为只有一台机器所以我们可以使用session进行用户信息的管理,也就是登录以后我们的用户信息存放在session中,然后同时生成了sessionId返回给浏览器,并且存放在cookie中,以这样的方式保证用户下次访问不会退出,同时设置session的过期时间为用户的登录进行过期操作。
二、集群/分布式环境
**单点登录:**在多个系统中,用户只需一次登录,各个系统即可感知该用户已经登录。
1.服务器Session复制
在分布式场景下,有一种常见的会话管理方式那就是使用session共享的方式进行会话的管理,它意味着当用户登录到某台机器以后,session会在各个节点间通过一定的通信方式进行session的同步,同步以后每个节点都会持有会话,这种方式在tomcat、jboss、jetty等web容器中都已经有所支持,如果使用tomcat可以通过配置tomcat的server.xml来开启tomcat集群功能,同时需要在web.xml配置分布式支持。(具体操作可以去网上自行搜索)
缺点:
1.如果有很多用户信息,session在网络之间的共享会占用很大的网络开销。
2.如果有网络延迟就会导致用户信息同步延迟,最终导致用户在某个节点登录以后,跳到另一个服务,用户信息并不存在。
2.定向流量分发(黏性Session)
会话黏连的方式依赖于定向流量分发(常见ip_hash方式),也就是用户和机器是绑定的,每个用户的ip是一定的,也就是当用户访问网站的时候通过负载均衡器(nginx)进行ip的hash算法,然后将该用户与固定的某个节点绑定,然后每次当用户访问这个系统的时候,都会定向路由到这个节点上,这样也就持有了登录的会话信息。这种方式的主要实现可以通过nginx的upstream模块配置ip_hash属性,以此来实现会话的黏连。
缺点:
1.缺乏容错性,如果当前访问的服务器发生故障,用户被转移到第二个服务器上时,他的session信息都将失效。
2.如果hash算法不好那么可能会导致某一台服务器压力较大,其他的服务器空闲。
3.Session共享机制
我们也可以使用redis或者是memcached进行会话的管理,也就是当我们在进行登录时候会先去认证服务器上进行用户的认证然后认证以后会将用户信息保存在redis中或者某台专门做memerycache的机器上,并且会下发一个token给到用户浏览器,这样当用户再次访问该网站的时候就会带着这个token去到服务器,此时服务器需要进行拦截认证,服务器获取到token以后先去redis中获取用户信息,如果能够获取到用户信息那么证明用户已经存在会话信息,可以继续操作,否则跳转登录页面。
redis解决方案:
memcached解决方案:
在memcached的解决方案中又分为粘性session和非粘性session两种处理方案。
粘性session处理:
粘性session的处理其实非常简单,就是将session信息在tomcat中保存一份同时在memcached中保存一份,每次当有用户请求进来以后先去tomcat的session中去验证用户信息,如果找不到再去memcached中寻找。
非粘性session处理:
非粘性session的处理就是tomcat本身不存储session写和读取都是从memcached进行。
可以使用用开源的msm插件解决memcached之间的session共享:Memcached_Session_Manager(MSM)。
4.Session持久化到数据库
这种方式也比较简单,就是每次登陆后创建session,然后将session的信息放入数据库,然后返回主键给客户端,这样下次验证的时候就可以带着主键访问,然后去数据库查找对应主键的session信息,如果可以找到证明有过会话,如果没有找到,证明没有这个session信息,其实数据库的方式有点类似于使用redis的方式或者是memerycache的方式进行管理,其实仔细想来原理都是一样的,就是找一个单独的服务器来存放session信息。
5.Terracotta实现Session复制
这种方式的原理就是采用Terracotta服务,每个tomcat作为Terracotta的客户端,当有session产生变化的时候客户端上报到Terracotta服务端,然后由Terracotta服务器把它转发给真正需要这个数据的节点。
6.无状态会话管理(JWT)
使用JWT的方式其实他的思想有些不同,它主要是一种无状态的思想,它将用户信息存放在客户端,也就是经过认证服务器以后会将用户信息进行简单的封装,然后通过一系列加密算法将,信息加密以后返回给前端,当用户再次访问的时候会将之前经过加密的用户信息放在请求头中,然后后端服务从请求头中获取,然后验证用户信息是否有效。
**JWT网站:**https://jwt.io/
JWT组成
- Header
- Payload
- Signature
头部信息(header)
作用:指定该JWT使用的签名
{
“alg”: “HS256”,// 签名算法
“typ”: “JWT” //token类型
}
将上面的json,用Base64URL 算法转成字符串,即为header。
消息体playload
也就是负载的信息
{
"exp" (expiration time):过期时间
"sub" (subject):主题,一般用用户id,用来标识用户会话
"iat" (Issued At):签发时间
}
这个 JSON 对象也要使用 Base64URL 算法转成字符串。
作用:JWT的请求数据
签名( signature)
Signature 部分是对前两部分的签名,防止数据篡改。
需要指定一个密钥(secret)。这个密钥只有服务器才知道,不能泄露给用户。然后,使用 Header 里面指定的签名算法(默认是 HMAC SHA256),按照下面的公式产生签名。
HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload),secret) header.payload.signature
头部、声明、签名用 . 号
最终:把 Header、Payload、Signature 三个部分拼成一个字符串,每个部分之间用"点"(.)分隔连在一起就得到了我们要的JWT
实现
pom依赖
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.7.0</version>
</dependency>
JWT工具类
package com.example.demo;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.Jwts;
/**
* @author zhangzhongguo
*/
public class JwtUtil {
/**
* 密钥,仅服务端存储
*/
private static String secret = "uoiqu99838*00j/jio14@!_!";
/**
* @param subject
* @param issueDate 签发时间
* @return
*/
public static String createToken(String subject, Date issueDate) {
Calendar c = Calendar.getInstance();
c.setTime(issueDate);
c.add(Calendar.DAY_OF_MONTH, 20);
String compactJws = Jwts.builder()
.setSubject(subject)
.setIssuedAt(issueDate)
.setExpiration(c.getTime())
.signWith(io.jsonwebtoken.SignatureAlgorithm.HS512, secret)
.compact();
return compactJws;
}
/**
* 解密 jwt
*
* @param token
* @return
* @throws Exception
*/
public static String parseToken(String token) {
try {
Claims claims = Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody();
if (claims != null) {
return claims.getSubject();
}
} catch (ExpiredJwtException e) {
e.printStackTrace();
System.out.println("jwt过期了");
}
return "";
}
public static void main(String[] args) {
String token = createToken("zhanghzongguo", new Date());
System.out.println(token);
}
}
Filter校验
package com.example.demo;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
@WebFilter(filterName = "authFilter", urlPatterns = "/**")
@Component
public class AuthFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
System.out.println("AAAAAAAAAAAA");
Filter.super.init(filterConfig);
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest req = (HttpServletRequest) servletRequest;
String token = req.getHeader("token");
if (token != null) {
// 判断解析token是否成功
String parseToken = JwtUtil.parseToken(token);
if (!StringUtils.isEmpty(parseToken)) {
System.out.println("auth success");
filterChain.doFilter(servletRequest, servletResponse);
}
} else {
System.out.println("auth failed");
}
}
@Override
public void destroy() {
}
}
Token携带方式
- http header
- url上的Get请求
- Cookies(会存在跨域问题)
- localStorage
安全框架(Shiro)
SpringBoot和Shiro整合
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--shiro相关-->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-web</artifactId>
<version>1.4.0</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.4.0</version>
</dependency>
</dependencies>
创建Realm:
package com.example.demo;
import org.apache.shiro.authc.*;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
public class MyShiroRealm extends AuthorizingRealm {
/**
* 授权
*
* @param principalCollection
* @return
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
System.out.println("jjjjjjjjjjjjjjjjjj");
return null;
}
/**
* 认证
*
* @param authenticationToken
* @return
* @throws AuthenticationException
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
System.out.println("00000000000000000000000000");
String username = (String) authenticationToken.getPrincipal();
if (!"javaboy".equals(username)) {
throw new UnknownAccountException("账户不存在!");
}
return new SimpleAuthenticationInfo(username, "123", getName());
}
}
创建shiro的配置类:
package com.example.demo;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.LinkedHashMap;
import java.util.Map;
@Configuration
public class ShiroConfig {
@Bean
MyShiroRealm myRealm() {
return new MyShiroRealm();
}
@Bean
SecurityManager securityManager() {
DefaultWebSecurityManager manager = new DefaultWebSecurityManager();
manager.setRealm(myRealm());
return manager;
}
@Bean
ShiroFilterFactoryBean shiroFilterFactoryBean() {
ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean();
bean.setSecurityManager(securityManager());
bean.setLoginUrl("/login");
bean.setSuccessUrl("/index");
bean.setUnauthorizedUrl("/unauthorizedurl");
Map<String, String> map = new LinkedHashMap<>();
map.put("/doLogin", "anon");
map.put("/**", "authc");
bean.setFilterChainDefinitionMap(map);
return bean;
}
}
创建登录类:
package com.example.demo;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.subject.Subject;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class LoginController {
@GetMapping("/doLogin")
public void doLogin(String username, String password) {
Subject subject = SecurityUtils.getSubject();
try {
subject.login(new UsernamePasswordToken(username, password));
System.out.println("登录成功!");
} catch (AuthenticationException e) {
e.printStackTrace();
System.out.println("登录失败!");
}
}
@GetMapping("/hello")
public String hello() {
return "hello";
}
@GetMapping("/login")
public String login() {
return "please login!";
}
}