分布式session和单点登录

1-分布式Session

1.1-什么是Session?

session 是一种服务端的会话机制。(被称为域对象)作为范围是一次会话的范围。

服务器为每个用户创建一个会话,存储用户的相关信息,以便多次请求能够定位到同一个上下文。这样,当用户在应用程序的 Web 页之间跳转时,存储在 Session 对象中的变量将不会丢失,而是在整个用户会话中一直存在下去。当用户请求来自应用程序的 Web 页时,如果该用户还没有会话,则 Web 服务器将自动创建一个 Session 对象。当会话过期或被放弃后,服务器将终止该会话。

Web开发中,web-server可以自动为同一个浏览器的访问用户自动创建session,提供数据存储功能。最常见的,会把用户的登录信息、用户信息存储在session中,以保持登录状态。

1.2-为什么会出现Session不一致?

在基于请求与响应的HTTP通讯中,当第一次请求来时,服务器端会接受到客户端请求,会创建一个session,使用响应头返回sessionid给客户端。浏览器获取到sessionid后会保存到本地cookie中。

当第二次请求来时,客户端会读取本地的sessionid,存放在请求头中,服务端在请求头中获取对象的sessionid在本地session内存中查询。

// 默认创建一个session,默认值为true,如果没有找到对象的session对象,就会创建该对象,并且将生成的sessionid 存入到响应头中。
HttpSession session = request.getSession();

@Override
public HttpSession getSession() {

if (request == null) {
throw new IllegalStateException(
sm.getString("requestFacade.nullRequest"));
}

return getSession(true);
}
// 默认情况下就是true,如果session不存在,则创建一个存入到本地,
// 假设修改为false会是什么样子的呢,就会关闭session功能。

但是session属于会话机制,当当先会话结束时,session就会被销毁,并且web程序会为每一次不同的会话创建不同的session,所以在分布式场景下,即使是调用同一个方法执行同样的代码,但是他们的服务器不同,自然web程序不同,整个上下文对象也不同,理所当然session也是不同的。

1.3-分布式Session的诞生

单服务器web应用中,session信息只需存在该服务器中,这是我们前几年最常接触的方式,但是近几年随着分布式系统的流行,单系统已经不能满足日益增长的百万级用户的需求,集群方式部署服务器已在很多公司运用起来,当高并发量的请求到达服务端的时候通过负载均衡的方式分发到集群中的某个服务器,这样就有可能导致同一个用户的多次请求被分发到集群的不同服务器上,就会出现取不到session数据的情况,于是session的共享就成了一个问题。

如上图,假设用户包含登录信息的session都记录在第一台web-server上,反向代理如果将请求路由到另一台web-server上,可能就找不到相关信息,而导致用户需要重新登录。

1.4-Session一致性解决方案

1. session复制(同步)

Tomcat自带该功能

思路:多个web-server 之间相互同步session,这样每个web-server之间都包含全部的session

优点web-server 支持的功能,应用程序不需要修改代码,应用服务器开启web容器的session复制功能,在集群中的几台服务器之间同步session对象,使得每台服务器上都保存所有的session信息,这样任何一台宕机都不会导致session的数据丢失,服务器使用session时,直接从本地获取。

不足

  • session 的同步需要数据传输,占内网带宽,有时延
  • 所有web-server 都包含所有session数据,数据量受内存限制,无法水平扩展
  • 有更多web-server 时要歇菜,在应用集群达到数千台的时候,就会出现瓶颈,每台都需要备份session,出现内存不够用的情况。

2. 利用cookie记录session

思路:session记录在客户端,每次请求服务器的时候,将session放在请求中发送给服务器,服务器处理完请求后再将修改后的session响应给客户端。这里的客户端就是cookie。

利用cookie记录session的也有缺点,比如受cookie大小的限制,能记录的信息有限;每次请求响应都需要传递cookie,影响性能,如果用户关闭cookie,访问就不正常。但是由于

cookie的简单易用,可用性高,支持应用服务器的线性伸缩,而大部分要记录的session信息比较小,因此事实上,许多网站或多或少的在使用cookie记录session。

优点:服务端不需要存储,简单易用

缺点

  • 每次http请求都携带session,占外网带宽
  • 数据存储在端上,并在网络传输,存在泄漏、篡改、窃取等安全隐患
  • session存储的数据大小受cookie限制

3. session绑定(反向代理hash一致性)

思路web-server为了保证高可用,有多台冗余,反向代理层能不能做一些事情,让同一个用户的请求保证落在一台web-server 上呢?

使用Nginx的负载均衡算法其中的hash_ip算法将ip固定到某一台服务器上,这样就不会出现session共享问题,因为同一个ip访问下,永远是同一个服务器。

这种方式不符合对系统的高可用要求,因为一旦某台服务器宕机,那么该机器上的session也就不复存在了,用户请求切换到其他机器后么有session,无法完成业务处理。

缺点:失去了Nginx负载均衡的初心。

优点

  • 只需要改nginx配置,不需要修改应用代码
  • 负载均衡,只要hash 属性是均匀的,多台web-server的负载是均衡的
  • 可以支持web-server水平扩展(session 同步法是不行的,受内存限制)

不足

  • 如果web-server重启,一部分session会丢失,产生业务影响,例如部分用户重新登录
  • 如果web-server水平扩展,rehashsession重新分布,也会有一部分用户路由不到正确的session

4. 后端统一集中存储

思路:将session存储在web-server后端的存储层,数据库或者缓存

优点

  • 没有安全隐患
  • 可以水平扩展,数据库/缓存水平切分即可
  • web-server重启或者扩容都不会有session丢失

不足:增加了一次网络调用,并且需要修改应用代码

对于db存储还是cache,个人推荐后者:session读取的频率会很高,数据库压力会比较大。如果有session高可用需求,cache可以做高可用,但大部分情况下session可以丢失,一般也不需要考虑高可用。

方案:使用Spring Session框架,相当于将Session之缓存到Redis中。

5. 使用Token的方式代替Session功能

在移动端,是没有Session这个概念的,都是使用Token的方式来实现的。

token最终会存放到Redis中,redis-cluster分片集群中是默认支持分布式共享的。完美的解决的共享问题。

1.5-使用Spring Session实现Session共享

1. 添加依赖

<!-- 引入springboot&redis整合场景 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- 引入springboot&springsession整合场景 -->
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
</dependency>

2. 添加相关配置

# redis配置
spring.redis.host=39.98.76.67
#spring.redis.pool.max-active=100
spring.redis.jedis.pool.max-idle=100

# springsession配置
spring.session.store-type=redis

# session过期时间
spring.session.timeout=1800

3. 配置redis服务连接

@EnableRedisHttpSession(maxInactiveIntervalInSeconds = 1800)//单位秒
public class SessionConfig {
@Value("${redis.hostname}")
private String hostName;

@Value("${redis.port}")
private int port;

@Value("${redis.password}")
private String password;

@Bean
public JedisConnectionFactory connectionFactory() {
JedisConnectionFactory connectionFactory = new JedisConnectionFactory();
connectionFactory.setPort(port);
connectionFactory.setHostName(hostName);
connectionFactory.setPassword(password);
return connectionFactory;
}
}

4. 新建初始化redis配置类

//初始化Session配置
public class SessionInitializer extends AbstractHttpSessionApplicationInitializer{
public SessionInitializer() {
super(SessionConfig.class);
}
}

5. 新建一个启动类

@SpringBootApplication
@RestController
public class TestSessionApp {

@Value("${server.port}")
private String serverPort;

// 创建session 会话
// http://127.0.0.1:8080/createSession?name=aaa
@RequestMapping("/createSession")
public String createSession(HttpServletRequest request, String name) {
HttpSession session = request.getSession();
System.out.println("存入Session sessionid:信息" + session.getId() + ",name:" + name + ",serverPort:" + serverPort);
session.setAttribute("name", name);
return serverPort + "success";
}

// 获取session 会话
@RequestMapping("/getSession")
public Object getSession(HttpServletRequest request) {
// 参数为true的情况下,客户端使用对应的sessionid查不到对应的session,会创建一个新的session
// 参数为false的情况下,客户端使用对应的sessionid查不到对应的session,不会创建一个新的session
HttpSession session = request.getSession(false);

if (session == null) {
return serverPort + "服务器上没有session...";
}

System.out.println("获取Session sessionid:信息" + session.getId() + ",serverPort:" + serverPort);
Object value = session.getAttribute("name");
return serverPort + "-" + value;
}

@RequestMapping("/getServerPort")
public String getServerPort () {
return serverPort;
}

public static void main(String[] args) {
SpringApplication.run(TestSessionApp.class, args);
}
}

当然最好的方式还是使用Token来代理session来实现分布式回话。

2-单点登录

2.1-什么是单点登录?

单点登录SSO(Single Sign On)说得简单点就是在一个多系统共存的环境下,用户在一处登录后,就不用在其他系统中登录,也就是用户的一次登录能得到其他所有系统的信任。

单点登录在大型网站里使用得非常频繁,例如,阿里旗下有淘宝、天猫、支付宝等网站,还有背后的成百上千的子系统,用户一次操作或交易可能涉及到几十个子系统的协作,如果每个子系统都需要用户认证,不仅用户会疯掉,各子系统也会为这种重复认证授权的逻辑搞疯掉。

所以,单点登录要解决的就是,用户只需要登录一次就可以访问所有相互信任的应用系统。

  • SSO服务端和SSO客户端直接是通过授权以后发放Token的形式来访问受保护的资源
  • 相对于浏览器来说,业务系统是服务端,相对于SSO服务端来说,业务系统是客户端
  • 浏览器和业务系统之间通过会话正常访问
  • 不是每次浏览器请求都要去SSO服务端去验证,只要浏览器和它所访问的服务端的会话有效它就可以正常访问

2.2-基于Cookie的单点登录

基于Cookie的单点登录核心原理:

将用户名密码加密之后存于Cookie中,之后访问网站时在过滤器(filter)中校验用户权限,如果没有权限则从Cookie中取出用户名密码进行登录,让用户从某种意义上觉得只登录了一次。

该方式缺点就是多次传送用户名密码,增加被盗风险,以及不能跨域。点击这里了解Java如何进行跨域。同时www.qiandu.com与mail.qiandu.com同时拥有登录逻辑的代码,如果涉及到修改操作,则需要修改两处。

2.4-统一认证中心方案原理

在生活中我们也有类似的相关生活经验,例如你去食堂吃饭,食堂打饭的阿姨(www.qiandu.com)告诉你,不收现金。并且告诉你,你去门口找换票的(passport.com)换小票。于是你换完票之后,再去找食堂阿姨,食堂阿姨拿着你的票,问门口换票的,这个票是真的吗?换票的说,是真的,于是给你打饭了。

基于上述生活中的场景,我们将基于Cookie的单点登录改良以后的方案如下:

经过分析,Cookie单点登录认证太过于分散,每个网站都持有一份登陆认证代码。于是我们将认证统一化,形成一个独立的服务。当我们需要登录操作时,则重定向到统一认证中心http://passport.com。于是乎整个流程就如上图所示:

第一步:用户访问www.qiandu.com。过滤器判断用户是否登录,没有登录,则重定向(302)到网站http://passport.com

第二步:重定向到passport.com,输入用户名密码。passport.com将用户登录的信息记录到服务器的session中。

第三步:passport.com给浏览器发送一个特殊的凭证,浏览器将凭证交给www.qiandu.comwww.qiandu.com则拿着浏览器交给他的凭证去passport.com验证凭证是否有效,从而判断用户是否登录成功

第四步:登录成功,浏览器与网站之间进行正常的访问。

2.5-Yelu大学研发的CAS(Central Authentication Server)

下面就以耶鲁大学研发的CAS为分析依据,分析其工作原理。首先看一下最上层的项目部署图:

部署项目时需要部署一个独立的认证中心(cas.qiandu.com),以及其他N个用户自己的web服务。

认证中心:也就是cas.qiandu.com,即cas-server。用来提供认证服务,由CAS框架提供,用户只需要根据业务实现认证的逻辑即可。

用户web项目:只需要在web.xml中配置几个过滤器,用来保护资源,过滤器也是CAS框架提供了,即cas-client,基本不需要改动可以直接使用。

CAS的详细登录流程

上图是3个登录场景,分别为:第一次访问www.qiandu.com、第二次访问、以及登录状态下第一次访问mail.qiandu.com。

下面就详细说明上图中每个数字标号做了什么,以及相关的请求内容,响应内容。

1. 第一次访问
  • 标号1:用户访问http://www.qiandu.com,经过他的第一个过滤器(cas提供,在web.xml中配置)AuthenticationFilter。
  • 过滤器全称:org.jasig.cas.client.authentication.AuthenticationFilter
  • 主要作用:判断是否登录,如果没有登录则重定向到认证中心。
  • 标号2:www.qiandu.com发现用户没有登录,则返回浏览器重定向地址。

首先可以看到我们请求www.qiandu.com,之后浏览器返回状态码302,然后让浏览器重定向到cas.qiandu.com并且通过get的方式添加参数service,该参数目的是登录成功之后会要重定向回来,因此需要该参数。并且你会发现,其实server的值就是编码之后的我们请求www.qiandu.com的地址。

  • 标号3:浏览器接收到重定向之后发起重定向,请求cas.qiandu.com。
  • 标号4:认证中心cas.qiandu.com接收到登录请求,返回登陆页面。

上图就是标号3的请求,以及标号4的响应。请求的URL是标号2返回的URL。之后认证中心就展示登录的页面,等待用户输入用户名密码。

  • 标号5:用户在cas.qiandu.com的login页面输入用户名密码,提交。
  • 标号6:服务器接收到用户名密码,则验证是否有效,验证逻辑可以使用cas-server提供现成的,也可以自己实现。

上图就是标号5的请求,以及标号6的响应了。当cas.qiandu.com即csa-server认证通过之后,会返回给浏览器302,重定向的地址就是Referer中的service参数对应的值。后边并通过get的方式挟带了一个ticket令牌,这个ticket就是ST(数字3处)。同时会在Cookie中设置一个CASTGC,该cookie是网站cas.qiandu.com的cookie,只有访问这个网站才会携带这个cookie过去。

Cookie中的CASTGC:向cookie中添加该值的目的是当下次访问cas.qiandu.com时,浏览器将Cookie中的TGC携带到服务器,服务器根据这个TGC,查找与之对应的TGT。从而判断用户是否登录过了,是否需要展示登录页面。TGT与TGC的关系就像SESSION与Cookie中SESSIONID的关系。点击这里了解Java如何操作Cookie。

  • TGT:Ticket Granted Ticket(俗称大令牌,或者说票根,他可以签发ST)
  • TGC:Ticket Granted Cookie(cookie中的value),存在Cookie中,根据他可以找到TGT。
  • ST:Service Ticket (小令牌),是TGT生成的,默认是用一次就生效了。也就是上面数字3处的ticket值。
  • 标号7:浏览器从cas.qiandu.com哪里拿到ticket之后,就根据指示重定向到www.qiandu.com,请求的url就是上面返回的url。

  • 标号8:www.qiandu.com在过滤器中会取到ticket的值,然后通过http方式调用cas.qiandu.com验证该ticket是否是有效的。

  • 标号9:cas.qiandu.com接收到ticket之后,验证,验证通过返回结果告诉www.qiandu.com该ticket有效。

  • **标号10:www.qiandu.com接收到cas-server的返回,知道了用户合法,展示相关资源到用户浏览器上。

至此,第一次访问的整个流程结束,其中标号8与标号9的环节是通过代码调用的,并不是浏览器发起,所以没有截取到报文。

2. 第二次访问

上面以及访问过一次了,当第二次访问的时候发生了什么呢?

  • 标号11:用户发起请求,访问www.qiandu.com。会经过cas-client,也就是过滤器,因为第一次访问成功之后www.qiandu.com中会在session中记录用户信息,因此这里直接就通过了,不用验证了。

  • 标号12:用户通过权限验证,浏览器返回正常资源。

3. 访问mail.qiandu.com
  • 标号13:用户在www.qiandu.com正常上网,突然想访问mail.qiandu.com,于是发起访问mail.qiandu.com的请求。
  • 标号14:mail.qiandu.com接收到请求,发现第一次访问,于是给他一个重定向的地址,让他去找认证中心登录。

上图可以看到,用户请求mail.qiandu.com,然后返回给他一个网址,状态302重定向,service参数就是回来的地址。

  • 标号15:浏览器根据14返回的地址,发起重定向,因为之前访问过一次了,因此这次会携带上次返回的Cookie:TGC到认证中心。
  • 标号16:认证中心收到请求,发现TGC对应了一个TGT,于是用TGT签发一个ST,并且返回给浏览器,让他重定向到mail.qiandu.com

可以发现请求的时候是携带Cookie:CASTGC的,响应的就是一个地址加上TGT签发的ST也就是ticket。

  • 标号17:浏览器根据16返回的网址发起重定向。
  • 标号18:mail.qiandu.com获取ticket去认证中心验证是否有效。
  • 标号19:认证成功,返回在mail.qiandu.com的session中设置登录状态,下次就直接登录。
  • 标号20:认证成功之后就反正用想要访问的资源了。

2.6-利用OAuth2实现单点登录

众所周知,在OAuth2在有授权服务器、资源服务器、客户端这样几个角色,当我们用它来实现SSO的时候是不需要资源服务器这个角色的,有授权服务器和客户端就够了。

授权服务器当然是用来做认证的,客户端就是各个应用系统,我们只需要登录成功后拿到用户信息以及用户所拥有的权限即可

之前我一直认为把那些需要权限控制的资源放到资源服务器里保护起来就可以实现权限控制,其实是我想错了,权限控制还得通过Spring Security或者自定义拦截器来做

1. Spring Security 、OAuth2、JWT、SSO

在本例中,一定要分清楚这几个的作用

首先,SSO是一种思想,或者说是一种解决方案,是抽象的,我们要做的就是按照它的这种思想去实现它

其次,OAuth2是用来允许用户授权第三方应用访问他在另一个服务器上的资源的一种协议,它不是用来做单点登录的,但我们可以利用它来实现单点登录。在本例实现SSO的过程中,受保护的资源就是用户的信息(包括,用户的基本信息,以及用户所具有的权限),而我们想要访问这这一资源就需要用户登录并授权,OAuth2服务端负责令牌的发放等操作,这令牌的生成我们采用JWT,也就是说JWT是用来承载用户的Access_Token的

最后,Spring Security是用于安全访问的,这里我们我们用来做访问权限控制

2. 认证服务器配置

(1)添加依赖
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.3.RELEASE</version>
<relativePath/><!-- lookup parent from repository -->
</parent>
<groupId>com.cjs.sso</groupId>
<artifactId>oauth2-sso-auth-server</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>oauth2-sso-auth-server</name>

<properties>
<java.version>1.8</java.version>
</properties>

<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security.oauth.boot</groupId>
<artifactId>spring-security-oauth2-autoconfigure</artifactId>
<version>2.1.3.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>

<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.8.1</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.56</version>
</dependency>

</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>

</project>

这里面最重要的依赖是:spring-security-oauth2-autoconfigure

(2)添加配置
spring:
datasource:
url:jdbc:mysql://localhost:3306/permission
username:root
password:123456
driver-class-name:com.mysql.jdbc.Driver
jpa:
show-sql:true
session:
store-type:redis
redis:
host:127.0.0.1
password:123456
port:6379
server:
port:8080
(3)AuthorizationServerConfig(重要)
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.security.core.token.DefaultToken;
import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer;
import org.springframework.security.oauth2.provider.token.DefaultTokenServices;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;
import org.springframework.security.oauth2.provider.token.store.JwtTokenStore;

import javax.sql.DataSource;

/**
* @author ChengJianSheng
* @date 2019-02-11
*/
@Configuration
@EnableAuthorizationServer
publicclass AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {

@Autowired
private DataSource dataSource;

@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
security.allowFormAuthenticationForClients();
security.tokenKeyAccess("isAuthenticated()");
}

@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.jdbc(dataSource);
}

@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints.accessTokenConverter(jwtAccessTokenConverter());
endpoints.tokenStore(jwtTokenStore());
// endpoints.tokenServices(defaultTokenServices());
}

/*@Primary
@Bean
public DefaultTokenServices defaultTokenServices() {
DefaultTokenServices defaultTokenServices = new DefaultTokenServices();
defaultTokenServices.setTokenStore(jwtTokenStore());
defaultTokenServices.setSupportRefreshToken(true);
return defaultTokenServices;
}*/

@Bean
public JwtTokenStore jwtTokenStore() {
returnnew JwtTokenStore(jwtAccessTokenConverter());
}

@Bean
public JwtAccessTokenConverter jwtAccessTokenConverter() {
JwtAccessTokenConverter jwtAccessTokenConverter = new JwtAccessTokenConverter();
jwtAccessTokenConverter.setSigningKey("cjs"); // Sets the JWT signing key
return jwtAccessTokenConverter;
}

}

说明:

  1. 别忘了**@EnableAuthorizationServer**
  2. Token存储采用的是JWT
  3. 客户端以及登录用户这些配置存储在数据库,为了减少数据库的查询次数,可以从数据库读出来以后再放到内存中
(4)WebSecurityConfig(重要)
import com.cjs.sso.service.MyUserDetailsService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

/**
* @author ChengJianSheng
* @date 2019-02-11
*/
@Configuration
@EnableWebSecurity
publicclass WebSecurityConfig extends WebSecurityConfigurerAdapter {

@Autowired
private MyUserDetailsService userDetailsService;

@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
}

@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/assets/**", "/css/**", "/images/**");
}

@Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin()
.loginPage("/login")
.and()
.authorizeRequests()
.antMatchers("/login").permitAll()
.anyRequest()
.authenticated()
.and().csrf().disable().cors();
}

@Bean
public PasswordEncoder passwordEncoder() {
returnnew BCryptPasswordEncoder();
}

}
(5)自定义登录页面
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;

/**
* @author ChengJianSheng
* @date 2019-02-12
*/
@Controller
publicclass LoginController {

@GetMapping("/login")
public String login() {
return"login";
}

@GetMapping("/")
public String index() {
return"index";
}

}

自定义登录页面的时候,只需要准备一个登录页面,然后写个Controller令其可以访问到即可,登录页面表单提交的时候method一定要是post,最重要的时候action要跟访问登录页面的url一样

千万记住了,访问登录页面的时候是GET请求,表单提交的时候是POST请求,其它的就不用管了

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>Ela Admin - HTML5 Admin Template</title>
<meta name="description" content="Ela Admin - HTML5 Admin Template">
<meta name="viewport" content="width=device-width, initial-scale=1">

<link type="text/css" rel="stylesheet" th:href="@{/assets/css/normalize.css}">
<link type="text/css" rel="stylesheet" th:href="@{/assets/bootstrap-4.3.1-dist/css/bootstrap.min.css}">
<link type="text/css" rel="stylesheet" th:href="@{/assets/css/font-awesome.min.css}">
<link type="text/css" rel="stylesheet" th:href="@{/assets/css/style.css}">

</head>
<body class="bg-dark">

<div class="sufee-login d-flex align-content-center flex-wrap">
<div class="container">
<div class="login-content">
<div class="login-logo">
<h1 style="color: #57bf95;">欢迎来到王者荣耀</h1>
</div>
<div class="login-form">
<form th:action="@{/login}" method="post">
<div class="form-group">
<label>Username</label>
<input type="text" class="form-control" name="username" placeholder="Username">
</div>
<div class="form-group">
<label>Password</label>
<input type="password" class="form-control" name="password" placeholder="Password">
</div>
<div class="checkbox">
<label>
<input type="checkbox"> Remember Me
</label>
<label class="pull-right">
<a href="#">Forgotten Password?</a>
</label>
</div>
<button type="submit" class="btn btn-success btn-flat m-b-30 m-t-30" style="font-size: 18px;">登录</button>
</form>
</div>
</div>
</div>
</div>


<script type="text/javascript" th:src="@{/assets/js/jquery-2.1.4.min.js}"></script>
<script type="text/javascript" th:src="@{/assets/bootstrap-4.3.1-dist/js/bootstrap.min.js}"></script>
<script type="text/javascript" th:src="@{/assets/js/main.js}"></script>

</body>
</html>
(6)定义客户端

(7)加载用户
import lombok.Data;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.User;

import java.util.Collection;

/**
* 大部分时候直接用User即可不必扩展
* @author ChengJianSheng
* @date 2019-02-11
*/
@Data
publicclass MyUser extends User {

private Integer departmentId; // 举个例子,部门ID

private String mobile; // 举个例子,假设我们想增加一个字段,这里我们增加一个mobile表示手机号

public MyUser(String username, String password, Collection<? extends GrantedAuthority> authorities) {
super(username, password, authorities);
}

public MyUser(String username, String password, boolean enabled, boolean accountNonExpired, boolean credentialsNonExpired, boolean accountNonLocked, Collection<? extends GrantedAuthority> authorities) {
super(username, password, enabled, accountNonExpired, credentialsNonExpired, accountNonLocked, authorities);
}
}
import com.alibaba.fastjson.JSON;
import com.cjs.sso.domain.MyUser;
import com.cjs.sso.entity.SysPermission;
import com.cjs.sso.entity.SysUser;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;

import java.util.ArrayList;
import java.util.List;

/**
* @author ChengJianSheng
* @date 2019-02-11
*/
@Slf4j
@Service
publicclass MyUserDetailsService implements UserDetailsService {

@Autowired
private PasswordEncoder passwordEncoder;

@Autowired
private UserService userService;

@Autowired
private PermissionService permissionService;

@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
SysUser sysUser = userService.getByUsername(username);
if (null == sysUser) {
log.warn("用户{}不存在", username);
thrownew UsernameNotFoundException(username);
}
List<SysPermission> permissionList = permissionService.findByUserId(sysUser.getId());
List<SimpleGrantedAuthority> authorityList = new ArrayList<>();
if (!CollectionUtils.isEmpty(permissionList)) {
for (SysPermission sysPermission : permissionList) {
authorityList.add(new SimpleGrantedAuthority(sysPermission.getCode()));
}
}

MyUser myUser = new MyUser(sysUser.getUsername(), passwordEncoder.encode(sysUser.getPassword()), authorityList);

log.info("登录成功!用户: {}", JSON.toJSONString(myUser));

return myUser;
}
}
(8)验证

当我们看到这个界面的时候,表示认证服务器配置完成

参考

——https://www.jianshu.com/p/b889f9a49fec

——https://mp.weixin.qq.com/s/kFNiSsNApLIXDChiUKo2TA

——https://mp.weixin.qq.com/s/opZK9j9CBPYdjGWQ6jH7EA