系统简介
Http协议
web应用采用browser/server架构,http作为通信协议。http是无状态协议,浏览器的每一次请求,服务器会独立处理,不与之前或之后的请求产生关联,这个过程用下图说明,三次请求/响应对之间没有任何联系。

但这也同时意味着,任何用户都能通过浏览器访问服务器资源,如果想保护服务器的某些资源,必须限制浏览器请求;要限制浏览器请求,必须鉴别浏览器请求,响应合法请求,忽略非法请求;要鉴别浏览器请求,必须清楚浏览器请求状态。既然http协议无状态,那就让服务器和浏览器共同维护一个状态吧!这就是会话机制。
有状态会话
浏览器第一次请求服务器,服务器创建一个会话,并将会话的id作为响应的一部分发送给浏览器,浏览器存储会话id,并在后续第二次和第三次请求中带上会话id,服务器取得请求中的会话id就知道是不是同一个用户了,这个过程用下图说明,后续请求与第一次请求产生了关联。 
服务器在内存中保存会话对象,浏览器怎么保存会话id呢?那就浏览器自己来维护这个会话id,每次发送http请求时浏览器自动发送会话id,cookie机制正好用来做这件事。cookie是浏览器用来存储少量数据的一种机制,数据以”key/value“形式存储,浏览器发送http请求时自动附带cookie信息 tomcat会话机制当然也实现了cookie,访问tomcat服务器时,浏览器中可以看到一个名为“JSESSIONID”的cookie,这就是tomcat会话机制维护的会话id,使用了cookie的请求响应过程如下图 
记录登陆状态
有了会话机制,登录状态就好明白了,我们假设浏览器第一次请求服务器需要输入用户名与密码验证身份,服务器拿到用户名密码去数据库比对,正确的话说明当前持有这个会话的用户是合法用户,应该将这个会话状态进行保存,例如:

单点登录系统设计
概述
web系统早已从久远的单系统发展成为如今由多系统组成的应用群,面对如此众多的系统,用户难道要一个一个登录、然后一个一个注销吗?就像下图描述的这样,例如 
Web系统由单系统发展成多系统组成的应用群,复杂性应该由系统内部承担,而不是用户。无论web系统内部多么复杂,对用户而言,都是一个统一的整体,也就是说,用户访问web系统的整个应用群与访问单个系统一样,登录/注销只要一次就够了  虽然单系统的登录解决方案很完美,但对于多系统应用群已经不再适用了,为什么呢? 单系统登录解决方案的核心是cookie,cookie携带会话id在浏览器与服务器之间维护会话状态。但cookie是有限制的,这个限制就是cookie的域(通常对应网站的域名),浏览器发送http请求时会自动携带与该域匹配的cookie,而不是所有cookie,例如:  既然这样,为什么不将web应用群中所有子系统的域名统一在一个顶级域名下,例如“*.baidu.com”,然后将它们的cookie域设置为“baidu.com”,这种做法理论上是可以的,甚至早期很多多系统登录就采用这种同域名共享cookie的方式。 然而,可行并不代表好,共享cookie的方式存在众多局限。首先,应用群域名得统一;其次,应用群各系统使用的技术(至少是web服务器)要相同,不然cookie的key值(tomcat为JSESSIONID)不同,无法维持会话,共享cookie的方式是无法实现跨语言技术平台登录的,比如java、php、.net系统之间;第三,cookie本身不安全。 因此,我们需要一种全新的登录方式来实现多系统应用群的登录,这就是单点登录, 什么是单点登录?单点登录全称Single Sign On(以下简称SSO),是指在多系统应用群中登录一个系统,便可在其他所有系统中得到授权而无需再次登录,包括单点登录与单点注销两部分。
登陆业务设计
相比于单系统登录,sso需要一个独立的认证中心,只有认证中心能接受用户的用户名密码等安全信息,其他系统不提供登录入口,只接受认证中心的间接授权。间接授权通过令牌实现,sso认证中心验证用户的用户名密码没问题,创建授权令牌,在接下来的跳转过程中,授权令牌作为参数发送给各个子系统,子系统拿到令牌,即得到了授权,可以借此创建会话,例如: 
创建项目聚合工程
创建聚合工程的目的是对项目中的资源(例如一些依赖)进行统一管理,多个项目module之间共享资源. 这次项目的maven工程结构如下: |—04-jt-sso |–sso-auth #认证服务器 |–sso-resource #资源服务器 |–pom.xml #公共依赖及版本管理
创建父工程
第一步:创建父工程,名字为04-jt-sso,例如:  第二步:删除工程中的src目录(parent工程一般不需要写java代码). 第三步:在pom.xml文件中添加parent元素并指定springboot依赖.
<parent>
<artifactId>spring-boot-starter-parent</artifactId>
<groupId>org.springframework.boot</groupId>
<version>2.3.2.RELEASE</version>
</parent>
创建认证工程
第一步:在04-jt-sso工程下创建sso-auth项目module,例如  第二步:打开pom.xml文件,添加项目依赖
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
</dependencies>
第三步:创建application.yml配置文件,定义服务端口,例如
server:
port: 8081
第四步:编写项目启动类.
package sso;
@SpringBootApplication
public class AuthApp {
public static void main(String[] args) {
SpringApplication.run(AuthApp.class,args);
}
}
第五步:启动服务检测是否可以启动ok.
创建资源工程
第一步:在04-jt-sso工程下创建sso-resource项目module,例如  第二步:打开pom.xml文件,添加项目依赖
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
</dependencies>
第三步:创建application.yml配置文件,定义服务端口,例如
server:
port: 8091
第四步:编写项目启动类.
package sso;
@SpringBootApplication
public class ResApp {
public static void main(String[] args) {
SpringApplication.run(ResApp.class,args);
}
}
认证服务实现
工具类
第一步:定义JWT工具类,用于创建,解析,验证token,代码如下:
package sso.auth.util;
public class JwtUtils {
private static String secret="AAABBBCCCDDDEEE";
public static String generatorToken(Map<String,Object> map){
return Jwts.builder()
.setClaims(map)
.setExpiration(new Date(System.currentTimeMillis()+30*60*1000))
.setIssuedAt(new Date())
.signWith(SignatureAlgorithm.HS256,secret)
.compact();
}
public static Claims getClaimsFromToken(String token){
return Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody();
}
public static boolean isTokenExpired(String token){
Date expiration=getClaimsFromToken(token).getExpiration();
return expiration.before(new Date());
}
}
第二步:定义Web工具类,用于向客户端响应json数据
package sso.auth.util;
public class WebUtils {
public static void writeJsonToClient(HttpServletResponse response, Map<String,Object> map)
throws IOException {
response.setCharacterEncoding("utf-8");
response.setContentType("application/json;charset=utf-8");
PrintWriter out=response.getWriter();
String result=new ObjectMapper().writeValueAsString(map);
out.println(result);
out.flush();
}
}
安全配置类
定义认证规则及异常处理,例如:
package sso.auth.config;
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Bean
public BCryptPasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable();
http.formLogin()
.successHandler(authenticationSuccessHandler())
.failureHandler(authenticationFailureHandler());
http.exceptionHandling()
.authenticationEntryPoint(authenticationEntryPoint());
http.authorizeRequests()
.anyRequest().authenticated();
}
public AuthenticationSuccessHandler authenticationSuccessHandler(){
return (httpServletRequest, httpServletResponse,authentication)-> {
User principal = (User)authentication.getPrincipal();
Map<String,Object> map=new HashMap<>();
map.put("state",200);
map.put("message","Login ok");
Map<String,Object> jwtMap=new HashMap<>();
jwtMap.put("username", principal.getUsername());
List<String> authorities = new ArrayList<>();
principal.getAuthorities().forEach((authority)-> {
authorities.add(authority.getAuthority());
});
jwtMap.put("authorities",authorities);
String token=JwtUtils.generatorToken(jwtMap);
map.put("token", token);
WebUtils.writeJsonToClient(httpServletResponse,map);
};
}
public AuthenticationFailureHandler authenticationFailureHandler(){
return (httpServletRequest, httpServletResponse, e) -> {
Map<String,Object> map=new HashMap<>();
map.put("state",500);
map.put("msg","username or password error");
WebUtils.writeJsonToClient(httpServletResponse,map);
};
}
public AuthenticationEntryPoint authenticationEntryPoint(){
return (httpServletRequest, httpServletResponse, e)->{
Map<String,Object> map=new HashMap<>();
map.put("state",401);
map.put("message","请先登录再访问");
WebUtils.writeJsonToClient(httpServletResponse,map);
};
}
}
认证逻辑对象
定义业务对象,处理客户端的登陆请求,例如:
package sso.auth.service;
@Service
public class UserDetailServiceImpl implements UserDetailsService {
@Autowired
private BCryptPasswordEncoder passwordEncoder;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
if(!"jack".equals(username))
throw new UsernameNotFoundException("user not exists");
String encodedPwd=passwordEncoder.encode("123456");
List<GrantedAuthority> grantedAuthorities =
AuthorityUtils.commaSeparatedStringToAuthorityList( "sys:res:retrieve,sys:res:create");
User user=new User(username,encodedPwd,grantedAuthorities);
return user;
}
}
此对象编写好以后,可以启动服务基于postman进行登陆访问测试。
资源服务实现
工具类
第一步:定义JWT工具类,主要用于解析JWT令牌,例如
package sso.resource.util;
public class JwtUtils {
private static String secret="AAABBBCCCDDDEEE";
public static String generatorToken(Map<String,Object> map){
return Jwts.builder()
.setClaims(map)
.setExpiration(new Date(System.currentTimeMillis()+30*60*1000))
.setIssuedAt(new Date())
.signWith(SignatureAlgorithm.HS256,secret)
.compact();
}
public static Claims getClaimsFromToken(String token){
return Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody();
}
public static boolean isTokenExpired(String token){
Date expiration=getClaimsFromToken(token).getExpiration();
return expiration.before(new Date());
}
}
第二步:定义Web工具类,用于向客户端响应json数据
package sso.auth.util;
public class WebUtils {
public static void writeJsonToClient(HttpServletResponse response, Map<String,Object> map)
throws IOException {
response.setCharacterEncoding("utf-8");
response.setContentType("application/json;charset=utf-8");
PrintWriter out=response.getWriter();
String result=new ObjectMapper().writeValueAsString(map);
out.println(result);
out.flush();
}
}
安全配置类
在资源服务中定义权限配置类,默认将所有认证请求放行,例如:
package sso.resource.config;
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable();
http.exceptionHandling()
.accessDeniedHandler(accessDeniedHandler());
http.authorizeRequests().anyRequest().permitAll();
}
public AccessDeniedHandler accessDeniedHandler(){
return (httpServletRequest, httpServletResponse, e)-> {
Map<String,Object> map=new HashMap<>();
map.put("state",403);
map.put("message","没有访问权限,请联系管理员");
WebUtils.writeJsonToClient(httpServletResponse,map);
};
}
}
资源服务对象
定义资源服务对象,用于处理客户端的资源访问服务,例如:
package sso.resource.controller;
@RestController
public class ResourceController {
@PreAuthorize("hasAuthority('sys:res:create')")
@RequestMapping("/doCreate")
public String doCreate(HttpServletResponse response){
return "create resource (insert data) ok";
}
@PreAuthorize("hasAuthority('sys:res:retrieve')")
@RequestMapping("/doRetrieve")
public String doRetrieve(){
return "query resource (select data) ok";
}
@PreAuthorize("hasAuthority('sys:res:update')")
@RequestMapping("/doUpdate")
public String doUpdate(){
return "update resource (update data) ok";
}
@PreAuthorize("hasAuthority('sys:res:delete')")
@RequestMapping("/doDelete")
public String doDelete(){
return "delete resource (dalete data) ok";
}
}
Spring MVC拦截器
资源服务器中的资源不是所有人都可以访问的,需要具备一定权限才可以,首先我们要判定是否登陆,然后判定登陆用户是否有权限,有访问权限才可以授权访问,这个操作可以放到spring mvc拦截器中进行实现,例如:
第一步:定义Spring MVC 拦截器.
package sso.resource.interceptor;
public class TokenInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String token=request.getHeader("token");
if(token==null||"".equals(token)) throw new RuntimeException("请先登陆");
if(JwtUtils.isTokenExpired(token)) throw new RuntimeException("请先登陆");
Claims claims=JwtUtils.getClaimsFromToken(token);
List<String> list = (List<String>) claims.get("authorities");
String[]authorities=list.toArray(new String[]{});
UserDetails userDetails= User.builder()
.username((String)claims.get("username"))
.password("")
.authorities(authorities)
.build();
PreAuthenticatedAuthenticationToken authenticationToken=
new PreAuthenticatedAuthenticationToken(
userDetails,userDetails.getPassword(),
AuthorityUtils.createAuthorityList(authorities));
authenticationToken.setDetails(new WebAuthenticationDetails(request));
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
return true;
}
}
第二步:创建Spring Web配置类,用于注册和配置Spring MVC拦截器,例如:
package sso.resource.config;
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new TokenInterceptor())
.addPathPatterns("/**");
}
}
资源访问测试
第一步:启动认证服务器,通过postman进行登陆认证,例如:
 第二步:启动资源服务器,并基于认证服务器返回的令牌进行资源访问 
创建通用工程
背景分析
当多个项目都有一部分公共资源需要重复编写时,我们可以创建一个公共工程,在这个工程中创建共性对象和依赖.其它工程需要时直接引用即可.
创建工程
{
UrlBasedCorsConfigurationSource configSource=new UrlBasedCorsConfigurationSource();
CorsConfiguration config=new CorsConfiguration();
config.addAllowedHeader("*");
config.addAllowedMethod("*");
config.addAllowedOrigin("*");
config.setAllowCredentials(true);
configSource.registerCorsConfiguration("/**", config);
FilterRegistrationBean<CorsFilter> fBean=
new FilterRegistrationBean(
new CorsFilter(configSource));
fBean.setOrder(Ordered.HIGHEST_PRECEDENCE);
return fBean;
}
}
引用通用工程
第一步:删除sso-auth,sso-resource 工程下的util包 第二步:删除sso-auth,sso-resource 工程中的公共依赖 第三步:在sso-auth,sso-resource工程中添加通用工程依赖
<dependency>
<groupId>com.cy.jt</groupId>
<artifactId>sso-common</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
启动工程测试
打开postman进行重新登录,访问资源测试.
创建前端工程
背景分析
我们做后端,一般在测试时直接基于postman进行访问就可以,为了更好理解前后端通讯过程,我们暂且基于springboot工程构建一个前端工程.
创建前端工程

初始化工程
第一步:添加web依赖,代码如下:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
第二步:创建application.yml配置文件,代码如下:
server:
port: 80
第三步:创建启动类,代码如下:
package sso;
@SpringBootApplication
public class UIApplication {
public static void main(String[] args) {
SpringApplication.run(UIApplication.class,args);
}
}
创建静态页面
将课前资料中的static目录直接拷贝到项目中的resource目录下.
登录页面login.html内容如下:
<!doctype html>
<html lang="en">
<head>
<!-- Required meta tags -->
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<!-- Bootstrap CSS -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" crossorigin="anonymous">
<title>login</title>
</head>
<body>
<div class="container"id="app">
<h3>Please Login</h3>
<form>
<div class="mb-3">
<label for="usernameId" class="form-label">Username</label>
<input type="text" v-model="username" class="form-control" id="usernameId" aria-describedby="emailHelp">
</div>
<div class="mb-3">
<label for="passwordId" class="form-label">Password</label>
<input type="password" v-model="password" class="form-control" id="passwordId">
</div>
<button type="button" @click="doLogin()" class="btn btn-primary">Submit</button>
</form>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/js/bootstrap.bundle.min.js" integrity="sha384-MrcW6ZMFYlzcLA8Nl+NtUVF0sA7MsXsP1UyJoMp4YLEuNSfAP+JcXn/tWtIaxVXM" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
<script>
var vm=new Vue({
el:"#app",
data:{
username:"",
password:""
},
methods: {
doLogin() {
let url = "http://localhost:8081/login"
var params = new URLSearchParams()
params.append('username',this.username);
params.append('password',this.password);
axios.post(url, params).then((response) => {
debugger
var data=response.data;
console.log(data);
if (data.state == 200) {
alert("login ok");
window.localStorage.setItem("token",data.token);
location.href="/index.html"
} else {
alert(response.message);
}
})
}
}
});
</script>
</body>
</html>
登录成功页面index.html,代码如下:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<div id="appIndex">
<h1>Index Page <a href="#" @click="doLogout()">Logout</a></h1>
<h2>CRUD(Create,Retrieve,Update,Delete) Operation</h2>
<ul>
<li><a href="#" @click="doCreate()">Create(添加-insert)</a></li>
<li><a href="#">Retrieve(查询-select)</a></li>
<li><a href="#" @click="doUpdate()">Update(更新-update)</a></li>
<li><a href="#">Delete(删除-delete)</a></li>
</ul>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
<script>
let vm=new Vue({
el:"#appIndex",
methods: {
doCreate() {
let url = "http://localhost:8091/doCreate"
let token=localStorage.getItem("token");
axios.get(url,{headers:{"token":token==null?"":token}}).then((response) => {
alert(response.data)
})
},
doUpdate() {
let url = "http://localhost:8091/doUpdate"
axios.get(url,{headers:{"token":localStorage.getItem("token")}}).then((response) => {
debugger
alert(response.data.message);
})
},
doLogout() {
localStorage.removeItem('token');
location.href="/login.html";
},
}
});
</script>
</body>
</html>
工程访问测试
启动sso-auth,sso-resource,sso-ui 工程,然后访问http://localhost/login.html进行登录  输入正确的账号,密码执行登录,登录成功以后跳转到如下页面.  然后,对create,update选项进行访问,检测输出结果.
资源访问过程分析
从登录认证,到资源访问,其过程如下: 
数据库访问操作
背景分析
目前我们登录时的账号,用户权限信息都是写死在UserServiceImpl类中的,实际项目中会从数据库查询用户以及用户对应权限信息.
业务及表设计
实际项目中用户权限控制,通常是通过用户,角色,菜单以及他们的关系表进行数据存储,其业务描述如下:  其sql脚本如下:
DROP DATABASE IF EXISTS `jt_security`;
CREATE DATABASE `jt_security` DEFAULT CHARACTER SET utf8mb4;
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
use `jt_security`;
CREATE TABLE `sys_menu` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'ID',
`name` varchar(50) NOT NULL COMMENT '权限名称',
`permission` varchar(200) DEFAULT NULL COMMENT '权限标识',
PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 COMMENT='权限表';
CREATE TABLE `sys_role` (
`id` bigint(11) NOT NULL AUTO_INCREMENT COMMENT '角色ID',
`role_name` varchar(50) NOT NULL COMMENT '角色名称',
PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 COMMENT='角色表';
CREATE TABLE `sys_role_menu` (
`id` bigint(11) NOT NULL AUTO_INCREMENT COMMENT 'ID',
`role_id` bigint(11) DEFAULT NULL COMMENT '角色ID',
`menu_id` bigint(11) DEFAULT NULL COMMENT '权限ID',
PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 COMMENT='角色与权限关系表';
CREATE TABLE `sys_user` (
`id` bigint(11) NOT NULL AUTO_INCREMENT COMMENT '用户ID',
`username` varchar(50) NOT NULL COMMENT '用户名',
`password` varchar(100) DEFAULT NULL COMMENT '密码',
`status` varchar(10) DEFAULT NULL COMMENT '状态 PROHIBIT:禁用 NORMAL:正常',
PRIMARY KEY (`id`) USING BTREE,
UNIQUE KEY `username` (`username`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 COMMENT='系统用户表';
CREATE TABLE `sys_user_role` (
`id` bigint(11) NOT NULL AUTO_INCREMENT COMMENT 'ID',
`user_id` bigint(11) DEFAULT NULL COMMENT '用户ID',
`role_id` bigint(11) DEFAULT NULL COMMENT '角色ID',
PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 COMMENT='用户与角色关系表';
INSERT INTO `sys_menu` VALUES (1, 'select users', 'sys:res:create');
INSERT INTO `sys_menu` VALUES (2, 'select menus', 'sys:res:retrieve');
INSERT INTO `sys_menu` VALUES (3, 'select roles', 'sys:res:delete');
INSERT INTO `sys_role` VALUES (1, 'ADMIN');
INSERT INTO `sys_role` VALUES (2, 'USER');
INSERT INTO `sys_role_menu` VALUES (1, 1, 1);
INSERT INTO `sys_role_menu` VALUES (2, 1, 2);
INSERT INTO `sys_role_menu` VALUES (3, 1, 3);
INSERT INTO `sys_role_menu` VALUES (4, 2, 1);
INSERT INTO `sys_user` VALUES (1,'admin','$2a$10$hIAewJVvpTdDSidROQmoXuBBucjLC7sxf7PDMWggZG49cKYhTXt16','NORMAL');
INSERT INTO `sys_user` VALUES (2,'user','$2a$10$hIAewJVvpTdDSidROQmoXuBBucjLC7sxf7PDMWggZG49cKYhTXt16','NORMAL');
INSERT INTO `sys_user_role` VALUES (1, 1, 1);
INSERT INTO `sys_user_role` VALUES (2, 2, 2);
项目初始化
第一步:在sso-auth工程中添加,如下依赖:
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.2.0</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.21</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
第二步:修改sso-auth工程中配置文件,添加访问数据库部分,例如:
spring:
datasource:
url: jdbc:mysql:///jt_security?serverTimezone=Asia/Shanghai&characterEncoding=utf8
username: root
password: root
driver-class-name: com.mysql.cj.jdbc.Driver
业务代码编写
第一步:创建UserMapper接口,并定义数据访问方法,代码如下:
package sso.dao;
@Mapper
public interface UserMapper {
@Select("select * from sys_user where username=#{username}")
Map<String,Object> selectUserByUsername(
@Param("username")String username);
@Select(" select distinct m.permission " +
" from sys_user u left join sys_user_role ur on u.id=ur.user_id " +
" left join sys_role_menu rm on ur.role_id=rm.role_id " +
" left join sys_menu m on rm.menu_id=m.id " +
" where u.id=#{id}")
List<String> selectUserPermissions(@Param("id") Long id);
}
第二步:修改UserDetailServiceImpl类添加数据库访问操作,例如:
package sso.service;
@Service
public class UserDetailServiceImpl implements UserDetailsService {
@Autowired
private BCryptPasswordEncoder passwordEncoder;
@Autowired
private UserMapper userMapper;
@Override
public UserDetails loadUserByUsername(String username)
throws UsernameNotFoundException {
Map<String,Object> userMap=userMapper.selectUserByUsername(username);
if(userMap==null)throw new UsernameNotFoundException("user not exists");
List<String> userPermissions=
userMapper.selectUserPermissions((Long)userMap.get("id"));
return new User(username,
(String)userMap.get("password"),
AuthorityUtils.createAuthorityList(
userPermissions.toArray(new String[]{})));
}
}
第三步,启动工程进行访问测试.
总结(summary)
重难点分析
- 单体架构中的登录设计
- 分布式架构中的单点登录设计
- SpringSecutiry在认证服务器和资源服务器中的配置
- JWT在认证授权系统中的应用
常见FAQ
- 传统单体架构方式的会话是是如何实现的?(Cookie+Session)
- 传统单体架构方式的登录在分布式架构中有什么缺陷?(cookie的跨域,session的共享)
- 分布式架构中的认证方式如何实现?(方式1:Session数据持久化,方式2:认证服务器创建令牌,客户端
存储令牌,资源服务端解析令牌) - 认证服务器用来做什么?(创建并响应令牌,设置认证机制-登录成功,失败,没有认证)
- 认证服务器的令牌基于什么规范进行创建?(JWT-JSON Web Token)
- 资源服务器你要做什么?(解析令牌,存储用户认证和权限信息,提供有条件的资源访问)
- SpringBoot工程中编写单元测试要注意什么?(包-启动类所在包或子包,注解-@SpringBootTest,@Test-org.junit.jupiter.api.Test)
- SecurityConfig的作用是什么?(配置认证规则,授权方式)
- UserDetailsService接口的作用是什么?(访问数据库用户信息以及用户对应的权限信息,并进行封装,底层会交给AuthenticationManager管理器去进行认证.)
- @EnableGlobalMethodSecurity 注解的作用是什么?(描述启动类或配置,用于告诉底层系统,假如方法上有 @PreAuthorize注解,则在方法层面启动权限检测,有权限则授权访问,没有权限则抛异常.)
- 客户端拿到JWT令牌以后,如何进行的存储?(localStorage)
- 客户端在访问资源服务时,如何将令牌传递到资源服务器?(将令牌放在ajax请求的请求头中)
BUG分析
- 401 (访问资源时还没有认证)
- 403 (访问资源时没有权限)
- NullPointerException (对象访问属性或方法时因为对象为空而出现的异常)
- ClassNotFoundException(类没有找到,假如不是自己类,检查对应的依赖.)
- …
|