客户端发送请求到网关进行验证,网关会转到auth认证授权中心,认证中心会完成用户信息的认证,完成认证会会基于UUID生成一个token,然后与用户信息绑定在一起存储到数据库.后续用户在访问资源时,基于token从数据库查询用户状态,这种方式因为要基于数据库存储和查询用户状态,所以性能表现一般.(一般可用于中小型企业,查询量较少的时候)
用户登录成功后,会基于JWT技术生成一个token(这里的token中包含了原本存在数据库的用户信息),用户信息可以存储到这个token中.后续用户在访问资源时,对token内容解析,检查登录状态以及权限信息,无须再访问数据库.
jwt.io
工程结构设计:
因为单点登录属于多系统微服务架构,因此所有的服务开启前都需要打开nacos注册中心,将服务注册到nacosz注册中心上。
user包含的业务是(后续会在auth服务中进行远程调用):
1)基于用户名查询用户信息
2)基于用户id查询用户权限
Log包含的业务是:
注意:表与表之间,多对多时,关系的维护方在中间关联表。一对多时,关系的维护方在多的那一个表。
通过用户ID查询用户对应的菜单权限,首先要找到用户对应的角色,通过角色ID再到菜单表中找到对应的菜单ID,再通过菜单ID找到对应的权限。
- 基于用户id查询用户权限,涉及到的表有:
* 1)tb_user_roles(用户角色关系表,可以在此表中基于用户id找到用户角色)
* 2)tb_role_menus(角色菜单关系表,可以基于角色id找到菜单id)
* 3)tb_menus(菜单表,菜单为资源的外在表现形式,在此表中可以基于菜单id找到权限标识>
* 基于如上三张表获取用户权限,有什么解决方案?
* 1)方案1:三次单表查询
* 2)方案2:嵌套查询
* 3)方案3:多表查询
解决方案:
#方案一:单表查询
#基于用户id查询用户对用的角色id(查询处的角色id可能是多个)
select role_id from tb_user_roles where user_id = 1;
#基于角色id查询用户对应的菜单id
select menu_id from tb_role_menus where role_id in (1);
#基于菜单id查询菜单权限标识
select permission from tb_menus where id in (1,2,3);
#方案二:嵌套查询
select permission from tb_menus where id in (
select menu_id from tb_role_menus where role_id in (
select role_id from tb_user_roles where user_id = 1));
#方案三:多表查询
select m.permission
from
tb_role_menus rm join tb_user_roles ur on rm.role_id=ur.role_id
join tb_menus m on rm.menu_id =m.id
where ur.role_id=1;
用户登陆时调用此工程对用户身份进行统一身份认证和授权
以目标为导向进行设计,写代码时要按照正常顺序进行书写。
原来执行登录操作时的过程:request–>filter(过滤器)–>servlet–>handler intercepter–>controller
而spring.security处理请求进行过滤时就会调用AuthenticationManager——>在调用UserDetailsServiceImpl方法进行登录操作。
也就是一般基于用户名和密码进行业务操作一般写在UserDetailsService中(这是官方规定的接口),UserDetailsServiceImpl实现UserDetailsService接口,重写接口的方法。
如果单纯的是我们自己写的UserDetailsServiceImpl,是不会主动交给认证管理器,因此要实现UserDetailsService接口,并实现其方法。然后去完成密码的比对操作
1)先设计UserDetailsServiceImpl类,从sso-system服务中获取用户信息,并对用户信息进行封装返回,交给认证管理器(AuthenticationManager)。
其中要使用到远程服务调用,因此需要创建进行远程调用的接口RemoteUserService(该接口负责远程调用)。
当远程调用时,根据业务需求要基于用户名查询用户信息,因此需要对user进行封装,需要创建pojo实体类对象User。
2)当用户使用用户名和密码进行登录时,会将用户的密码与数据库中加密的密码进行比对,因此需要将用户传输的密码进行加密处理,需要添加配置类。
先设计一个SecurityConfig ,Bean出passwordEncoder对象用来构建密码加密对象,登录时,系统底层会基于此对象进行密码加密。
Security 认证流程分析
当用户发送登录请求的时候,进入到由多个过滤器构成的过滤链进行对用户和密码简单的校验,再提交到认证管理器,再通过认证管理器调用到的从数据库中取到的数据进行比对。而从数据库中取出的密码是加密的形式,而用户输入密码传到管理器不是加密的形式,不能直接比对,因此就需要在认证管理器中对用户输入进来的密码进行加密,所以要配置一个可以对密码进行加密的算法,数据库中的密码进行加密使用的算法是BCryptPasswordEncoder()的算法,因此就有了SecurityConfig配置类进行密码加密,并且使用BCry的加密算法。
底层所调用数据库数据的逻辑:
ProviderManager是AuthenticationManager认证管理器的实现类,认证管理器主要的作用就是密码的比对,身份的认证,再去调用AuthenticationProvider中的DaoAuthenticationProvider认证服务提供方,DaoAuthenticationProvider调用UserDetailsService接口中实现类的方法,主要是去访问数据库,我们自己写了一个UserDetailsServiceImpl类去实现UserDetailsService接口,重写其方法去调用数据库
单体架构的登录系统
到目前为止就是一个简单的单体架构的登录,
3)构建令牌生成及配置对象
本次我们借助JWT(Json Web Token-是一种json格式)方式将用户相关信息进行组织和加密,并作为响应令牌(Token),从服务端响应到客户端,客户端接收到这个JWT令牌之后,将其保存在客户端(例如localStorage),然后携带令牌访问资源服务器,资源服务器获取并解析令牌的合法性,基于解析结果判定是否允许用户访问资源.
因此写一个 TokenConfig类,用来配置令牌的生成,存储策略,验签方式(令牌合法性)。
4)构建oauth2Config配置类 负责将所有的认证和授权相关配置进行整合
业务方面:
package com.jt.auth.service;
import com.jt.auth.pojo.User;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import java.util.List;
/**
* Feign远程调用接口,在接口中对sso-system进行远程服务调用
* */
@FeignClient(value = "sso-system",contextId ="remoteUserService" )
public interface RemoteUserService {
/**
* 基于用户名查询用户信息
* */
@GetMapping("/user/login/{username}")
User selectUserByUsername(@PathVariable("username") String username);
/**
* 基于用户id获取用户权限
* */
@GetMapping("/user/permission/{userId}")
List<String> selectUserPermissions(@PathVariable("userId") Long userId);
}
package com.jt.auth.service;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import java.util.List;
@Slf4j
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired //注入的是负责远程调用的接口
private RemoteUserService remoteUserService;
/**
* 我们执行登录操作时,提交登录按钮系统会调用此方法
* 参数:username来自客户端用户提交的用户名
* 返回值:封装了登录用户信息以及用户权限信息的一个对象,返回的UserDetails对象最终会交给认证管理器
* */
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//1.基于用户名查找用户信息,判断用户名是否存在
com.jt.auth.pojo.User user= remoteUserService.selectUserByUsername(username);
if(user==null)
throw new UsernameNotFoundException("用户不存在");
//2.基于用户id查询用户权限(登录用户不一定可以访问所有资源)
List<String> permissions = remoteUserService.selectUserPermissions(user.getId());
//可以打印出有哪些权限信息
log.info("permissions {}",permissions.toString());
//3.封装查询结果并返回
//这里为什么要new User(),需要查UserDetailsService接口实现类底层源码。
return new User(user.getUsername(),
user.getPassword(),
AuthorityUtils.createAuthorityList(permissions.toArray(new String[]{})));
}
}
使用AuthorityUtils.createAuthorityList()可以转化成参数所需的类型,()中里要填写的是string…数组,因此就需要将peimissions转化为数组——permissions.toArray(newString[]{})。
@Configuration
public class SecurityConfig {
}
//构建密码加密对象,登录时,系统底层会基于此对象进行密码加密
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
要继承WebSecurityConfigurerAdapter,重写其中的configure(HttpSecurity http)方法——这个方法会在服务启动时就会启动,进行自定义的配置
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception{
//super.configure(http);//默认所有请求都要认证,不需要
//1.禁用跨域攻击(先这么写,不写会报403异常,这里的登录默认是post请求,但系统底层的跨域设计是不允许post请求,)
http.csrf().disable();
//2.放行所有资源的访问(后续可以基于选择对资源进行认证和放行)此处是根据业务需求进行选择
//3.自定义登录成功和失败以后的处理逻辑(目的是响应到客户端的数据是json字符串)
//默认没有配置,会跳转到登录成功或失败的页面,实际上前后端分离架构中服务端不负责页面跳转,只负责返回json数据
http.formLogin()//登录表单,此方法执行后会生成一个/login的url
//登录成功处理器successHandler
.successHandler(successHandler())//启动时就会调用,并不会登录成功才调用
.failureHandler(failureHandler());
}
}
//定义认证成功处理器
//登录成功以后返回json数据
@Bean
public AuthenticationSuccessHandler successHandler() {
return (request, response, authentication) -> {
//定义响应数据
Map<String,Object> map = new HashMap<>();
map.put("state",200);
map.put("message","login success");
//将响应数据写到客户端,调用writeJsonToClient方法
writeJsonToClient(response,map);
};
}
//定义登录失败处理器
@Bean
public AuthenticationFailureHandler failureHandler() {
return (request,response,exception)->{
//构建map对象封装到要响应到客户端的数据
Map<String,Object> map=new HashMap<>();
map.put("state",500);
map.put("message", "login error");
//将map对象转换为json格式字符串并写到客户端
writeJsonToClient(response,map);
};
}
private void writeJsonToClient(
HttpServletResponse response,
Map<String,Object> map) throws IOException {
//将要响应到客户端的数据转化成json格式的字符串
String json=new ObjectMapper().writeValueAsString(map);
//设置响应数据的编码方式
response.setCharacterEncoding("utf-8");
//告诉浏览器响应数据的类型以及编码
response.setContentType("application/json;charset=utf-8");
//将字符串内容响应到客户端
PrintWriter out=response.getWriter();
out.println(json);
out.flush();//刷新
}
即判定用户身份信息的合法性,在基于oauth2协议完成认证时,需要此对象,所以这里将此对象拿出来交给spring管理
@Bean
@Override
public AuthenticationManager authenticationManager() throws Exception {
return super.authenticationManager();
}
package com.jt.auth.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
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;
@Configuration
public class TokenConfig {
/**
* 定义令牌存储的方案,本次选择基于JWT令牌方式存储用户状态
* */
@Bean
public TokenStore tokenStore(){
//这里采用JWT方式生成和存储令牌信息
return new JwtTokenStore(jwtAccessTokenConverter());
}
/**
* 配置JWT令牌和解析对象
* */
@Bean
public JwtAccessTokenConverter jwtAccessTokenConverter() {
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
converter.setSigningKey(SIGNING_KEY);//设置密钥
return converter;
}
/**
* JWT 令牌签名时使用的密钥(可以理解为盐值加密中的盐)
* 1)生成的令牌需要这个密钥进行签名
* 2)获取的令牌需要使用这个密钥进行验签(校验令牌合法性,是否被篡改过)
*/
//这里的签名key将来可以写到配置中心
private static final String SIGNING_KEY="auth";
}
适配器模式:不需要重写方法,适配器充当一个转化的作用,需要哪个方法,在进行重写。
//AuthorizationServerConfigurerAdapter认证授权的适配器 ——用到了适配器模式
//因此Oauth2Config继承这个接口,不需要重写全部的方法,需要哪个方法在进行定义即可
@AllArgsConstructor //生成一个全参构造函数
@EnableAuthorizationServer //启动认证和授权
@Configuration
public class Oauth2Config extends AuthorizationServerConfigurerAdapter {
//为下文需要调用的进行注入。
//1.可以给每个上面都加入@Autowired
//2.加入一个全参构造函数,或者在类上面加一个@AllArgsConstructor //生成一个全参构造函数
private AuthenticationManager authenticationManager;//在SecurityConfig @Bean一个authenticationManager()
private UserDetailsServiceImpl userDetailsService;//调用的是service层中的UserDetailsServiceImpl类
private TokenStore tokenStore;//在TokenConfig @Bean tokenStore()
private PasswordEncoder passwordEncoder;//在SecurityConfig @Bean passwordEncoder()加密的算法
private JwtAccessTokenConverter jwtAccessTokenConverter;//在TokenConfig @Bean
}
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
//super.configure(endpoints);
endpoints
//配置由谁完成认证?(认证管理器)
.authenticationManager(authenticationManager)
//谁负责查询用户业务数据(可写可不写。认证时需要两部分信息:一部分来自客户端,一部分来自数据库)
.userDetailsService(userDetailsService)
//支持对什么请求进行认证(可写可不写。默认支持post方式)
.allowedTokenEndpointRequestMethods(HttpMethod.GET,HttpMethod.POST)
//认证成功以后令牌如何生成和存储?(若不写默认生成普通令牌UUID.randomUUID(),存储方式为内存)
.tokenServices(tokenService());
}
@Bean
public AuthorizationServerTokenServices tokenService() {
//1.创建授权令牌服务对象(TokenServices)
DefaultTokenServices tokenServices = new DefaultTokenServices();
//2.配置令牌的存储(tokenStore)
tokenServices.setTokenStore(tokenStore);
//3.配置令牌增强(不进行配置,就是默认普通令牌,使用的就是UUID)
TokenEnhancerChain tokenEnhancer=new TokenEnhancerChain();
tokenEnhancer.setTokenEnhancers(Arrays.asList(jwtAccessTokenConverter));
tokenServices.setTokenEnhancer(tokenEnhancer);
//4.设置令牌的有效时间
tokenServices.setAccessTokenValiditySeconds(3600);//1小时
//5.设置令牌刷新策略(是否支持使用刷新令牌在生成新令牌)
tokenServices.setSupportRefreshToken(true);
//6.设置刷新令牌有效时长
tokenServices.setRefreshTokenValiditySeconds(3600*5);//5小时
return tokenServices;
}
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
//super.configure(clients);
clients.inMemory()
//定义客户端要携带的id(客户端访问此服务时要携带的id,这个是自己定义的字符串)
.withClient("gateway-client")
//定义客户端要携带的秘钥(这个秘钥也 是官方定义的一个规则,客户端要携带,字符串可与自己)
.secret(passwordEncoder.encode("123456"))
//定义作用范围(所有符合定义规则的客户端,例如:client、secret。。。)
.scopes("all")
//定义允许的认证方式(基于密码和刷新令牌的方式实现认证)
.authorizedGrantTypes("password","refresh_token");
}
//配置要对外暴露的认证URL,刷新令牌的URL,检查令牌的URL
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
//super.configure(security);
//1.定义(公开)要认证的url(permitAll()是官方定义好的)
//公开oauth/token_key端点
security.tokenKeyAccess("permitAll()")
//公开检查token有效性的URL
.checkTokenAccess("permitAll()")
//允许通过表单提交方式进行认证
.allowFormAuthenticationForClients();
}
上述的固定参数key要和以下配置类中定义的数据保持一致,否则会出现401错误
资源服务工程为一个业务数据工程,此工程中数据在访问通常情况下是受限访问,例如有些资源有用户,可以访问,有些资源必须认证才可访问,有些资源认证后,有权限才可以访问。
用户访问资源时的认证,授权流程设计如下
如何设计对这个资源工程的访问?
1.feign进行远程调用
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
2.nacos 服务注册
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
3.Nacos 服务配置
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
4.SSO技术方案:Spring Security+JWT+oauth2
该依赖中包含Spring Security(安全)、actuator(安全检查)、Spring Security jwt(生成令牌)、Spring Security oauth2(安全协议、定义一个规范)的依赖。
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>
资源服务器中添加该依赖只做授权,不做认证,添加完此依赖以后,在项目中我们要做哪些事情?对受限访问的资源可以先判断是否登录了,已经认证用户还要判断是否有权限?
5.Spring Web依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
1.Pojo实现类中自动生成UID——序列化ID的唯一标识
2.在IDEA中打开数据库并管理表信息