昨天我们的文件上传Demo已经比较完善了,今天我们来学习单点登录,更加的完善它,那么什么是单点登录呢?
背景传统的登录系统中,每个站点都实现了自己的专用登录模块。各站点的登录状态相互不认可,各站点需要逐一手工登录。这样的系统,我们又称之为多点登陆系统。应用起来相对繁琐(每次访问资源服务都需要重新登陆认证和授权)。与此同时,系统代码的重复也比较高。由此单点登陆系统诞生。
一、概述百度百科:
通俗易懂:
单点登录SSO(Single Sign On)说得简单点就是在一个多系统共存的环境下,用户在一处登录后,就不用在其他系统中登录,也就是用户的一次登录能得到其他所有系统的信任。单点登录在大型网站里使用得非常频繁,例如像阿里巴巴这样的网站,在网站的背后是成百上千的子系统,用户一次操作或交易可能涉及到几十个子系统的协作,如果每个子系统都需要用户认证,不仅用户会疯掉,各子系统也会为这种重复认证授权的逻辑搞疯掉。实现单点登录说到底就是要解决如何产生和存储那个信任,再就是其他系统如何验证这个信任的有效性,因此要点也就以下两个:
- 存储信任
- 验证信任
前面我们已经有了上传、网关、页面服务,接下来创建单点登录最重要的授权服务
1.添加pom依赖web、oath2、nacosdiscovery、nacosconfig
2.构建配置文件org.springframework.boot spring-boot-starter-weborg.springframework.cloud spring-cloud-starter-oauth2com.alibaba.cloud spring-cloud-starter-alibaba-nacos-discoverycom.alibaba.cloud spring-cloud-starter-alibaba-nacos-config
server:
port: 8071
spring:
application:
name: sca-auth
cloud:
nacos:
discovery:
server-addr: localhost:8848
config:
server-addr: localhost:8848
3.项目启动类
@SpringBootApplication
public class ResourceAuthApplication {
public static void main(String[] args) {
SpringApplication.run(ResourceAuthApplication.class, args);
}
}
4.启动项目
项目启动时生成一个密码(oauth2的作用)
访问我们配置的端口号出现如下界面
账号默认为user,密码就是项目启动时生成的密码,登录成功后会返回404,这表示登录成功,因为我们没有页面,这里我们可以自己配置一个页面,不过没啥必要
5.自定义登录业务逻辑图:
我们在实现登录时,会在UI工程中,定义登录页面(login.html),然后在页面中输入自己的登陆账号,登陆密码,将请求提交给网关,然后网关将请求转发到auth工程,登陆成功和失败要返回json数据,在这个章节我们会按这个业务逐步进行实现。
定义安全配置类修改SecurityConfig配置类,添加登录成功或失败的处理逻辑
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
//初始化加密对象
//此对象提供了不可逆的加密方式,相对于MD5更加安全
@Bean
public BCryptPasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
//登录成功处理器
@Bean
public AuthenticationSuccessHandler successHandler(){
return (request,response,authentication) ->{
//1.构建map对象,封装响应数据
Map map=new HashMap<>();
map.put("state",200);
map.put("message","login ok");
//2.将map对象写到客户端
writeJsonToClient(response,map);
};
}
//登录失败处理器
@Bean
public AuthenticationFailureHandler failureHandler(){
return (request,response, exception)-> {
//1.构建map对象,封装响应数据
Map map=new HashMap<>();
map.put("state",500);
map.put("message","login failure");
//2.将map对象写到客户端
writeJsonToClient(response,map);
};
}
private void writeJsonToClient(HttpServletResponse response,Object object) throws IOException {
//1.将对象转换为json
//将对象转换为json有3种方案:
//1)Google的Gson-->toJson (需要自己找依赖)
//2)阿里的fastjson-->JSON (spring-cloud-starter-alibaba-sentinel)
//3)Springboot web自带的jackson-->writevalueAsString (spring-boot-starter-web)
//我们这里借助springboot工程中自带的jackson
//jackson中有一个对象类型为ObjectMapper,它内部提供了将对象转换为json的方法
//例如:
String jsonStr=new ObjectMapper().writevalueAsString(object);
//3.将json字符串写到客户端
PrintWriter writer = response.getWriter();
writer.println(jsonStr);
writer.flush();
}
//创建认证管理器对象,负责完成用户信息的认证,判定用户身份信息的合法性,在基于oauth2协议完成认证时,需要此对象,所以这里拿出来给spring管理
@Bean
public AuthenticationManager authenticationManagerBean()
throws Exception {
return super.authenticationManagerBean();
}
}
定义用户处理对象
处理用户对象首先要获取用户信息,而用户信息我们存在数据库中,所以我们要先设计数据库表
数据库设计:
这里我们就需要知道权限的设计思路,一般都是分为用户、角色、与权限三个部分,一个用户可以有多个角色,而一个角色又可以对应不同权限,分析上述我们最少应该创建用户信息表、用户角色对应表、角色表、角色权限对应表、权限表五张表,分析清楚了我们接下来进行建表
用户表
根据用户输入的账号密码进行比对判断是否可以登录
角色表
权限表
用户角色对应表
角色权限对应表
数据库表我们就建好了,我们又怎么可以获取表中信息呢?所以我们要写一个feign接口,跨服务调用一下sql,进行用户信息和权限的查询,跨服务调用是因为整体的结构不过于混乱
创建新服务system负责数据库查询1.添加pom依赖
mysql mysql-connector-javacom.baomidou mybatis-plus-boot-starter3.3.1 com.alibaba.cloud spring-cloud-starter-alibaba-nacos-discoverycom.alibaba.cloud spring-cloud-starter-alibaba-nacos-configorg.springframework.boot spring-boot-starter-web
2.构建配置文件
server:
port: 8061
spring:
application:
name: sca-system
cloud:
nacos:
discovery:
server-addr: localhost:8848
config:
server-addr: localhost:8848
file-extension: yml
datasource:
url: jdbc:mysql:///jt-sso?serverTimezone=Asia/Shanghai&characterEncoding=utf8
username: root
password: root
3.启动类
略
4.Pojo
@Data
//@TableName 自己写sql不用写这些
public class User implements Serializable {
private static final long serialVersionUID = 4831304712151465443L;
//@TableId(type = IdType.AUTO)
private Integer id;
private String username;
private String password;
private String status;
}
5.controller
@RestController
@RequestMapping("user")
public class UserController {
@Autowired
private UserService userService;
@GetMapping("login/{username}")
public User doSelectUserByUsername(@PathVariable ("username") String username){
return userService.selectUserByUsername(username);
}
@GetMapping("permission/{userId}")
public List doSelectUserPermissions(@PathVariable("userId") Integer userId){
return userService.selectUserPermissions(userId);
}
@PostMapping("insert")
public void doinsert(@RequestBody User user){
userService.insert(user);
}
}
查询用户信息、权限、和新增三部分
6.Service
@Service
public interface UserService {
User selectUserByUsername(String username);
List selectUserPermissions(Integer userId);
void insert(User user);
}
@Service
public class UserServiceImpl implements UserService{
@Autowired
private BCryptPasswordEncoder bCryptPasswordEncoder;
@Autowired
private UserMapper userMapper;
@Override
public User selectUserByUsername(String username) {
if(username==null)return null;
return userMapper.selectUserByUsername(username);
}
@Override
public List selectUserPermissions(Integer userId) {
return userMapper.selectUserPermissions(userId);
}
//注册
@Override
public void insert(User user) {
user.setPassword(bCryptPasswordEncoder.encode(user.getPassword()));
user.setStatus("NORMAL");
userMapper.insert(user);
}
}
7.Mapper
@Mapper public interface UserMapper extends baseMapper{ @Select("select id,username,password,status " + "from tb_users " + "where username=#{username}") User selectUserByUsername(String username); @Select("select distinct m.permission " + "from tb_user_roles ur join tb_role_menus rm on ur.role_id=rm.role_id" + " join tb_menus m on rm.menu_id=m.id " + "where ur.user_id=#{userId}") List selectUserPermissions(Integer userId); }
MybatisPlus的最大缺点就是不能多表查询,所以我们用注解的方法写sql。
创建feign接口负责服务的调用@FeignClient(name = "sca-system",contextId = "remoteUserService")//后面记得加访问失败时跳转的界面
public interface RemoteUserService {
//查找用户信息
@GetMapping("user/login/{username}")
User selectUserByUsername(@PathVariable("username") String username);
//查询权限
@GetMapping("user/permission/{userId}")
List selectUserPermissions(@PathVariable("userId") Integer userId);
}
用户处理对象的前置工作都做好了,我们接下来写用户处理对象
用户处理对象在spring security应用中底层会借助UserDetailService对象获取数据库信息,并进行封装,最后返回给认证管理器,完成认证操作,通过feign调用system服务
@Slf4j
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
private BCryptPasswordEncoder passwordEncoder;
@Autowired
private RemoteUserService remoteUserService;
@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.查询用户权限信息
List permissions = remoteUserService.selectUserPermissions(user.getId());
log.info("permission {}",permissions);
System.out.println("permissions"+permissions);
List authorityList = AuthorityUtils.createAuthorityList(permissions.toArray(new String[]{}));
// 3.封装用户信息并返回
//密码必须是加密后的
System.out.println(user.getPassword());
return new User(username, user.getPassword(), authorityList);
}
}
构建令牌配置对象
借助JWT(Json Web Token-是一种json格式)方式将用户相关信息进行组织和加密,并作为响应令牌(Token),从服务端响应到客户端,客户端接收到这个JWT令牌之后,将其保存在客户端(例如localStorage),然后携带令牌访问资源服务器,资源服务器获取并解析令牌的合法性,基于解析结果判定是否允许用户访问资源.
@Configuration
public class TokenConfig {
//JWT令牌签名时使用的密钥(盐值)
private String SIGNING_KEY = "auth";
//配置令牌的存储策略,对于oauth2规范中提供了集中策略
@Bean
public TokenStore tokenStore() {
return new JwtTokenStore(jwtAccessTokenConverter());
}
//定义Jwt转换器,负责生成jwt令牌,解析令牌内容,及验签方式
@Bean
public JwtAccessTokenConverter jwtAccessTokenConverter(){
JwtAccessTokenConverter converter=new JwtAccessTokenConverter();
//设置加密/解密口令
converter.setSigningKey(SIGNING_KEY);
return converter;
}
}
整合类
完成所有配置的组装,在这个配置类中完成认证授权,JWT令牌签发等配置操作
SpringSecurity (提供认证和授权的实现)
TokenConfig(提供了令牌的生成,存储,校验方式)
Oauth2(定义了一套认证规范,例如为谁发令牌,都发什么内容,...)
继承AuthorizationServerConfigurerAdapter类,并重写里面的三个configure方法完成各项功能
@AllArgsConstructor //lombok全参构造
@Configuration
@EnableAuthorizationServer //开启认证和授权服务
public class Oauth2Config extends AuthorizationServerConfigurerAdapter{
//此对象负责完成认证管理
private AuthenticationManager authenticationManager;
//TokenStore负责完成令牌创建,信息读取
private TokenStore tokenStore;
//JWT令牌转换器(基于用户信息构建令牌,解析令牌)
private JwtAccessTokenConverter jwtAccessTokenConverter;
//密码加密匹配器对象
private PasswordEncoder passwordEncoder;
//负责获取用户信息信息
private UserDetailsService userDetailsService;
//设置认证端点的配置(/oauth/token),客户端通过这个路径获取JWT令牌
//去哪认证,认证细节配置
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints
//配置认证管理器
.authenticationManager(authenticationManager)
//验证用户的方法获得用户详情
.userDetailsService(userDetailsService)
//要求提交认证使用post请求方式,提高安全性
.allowedTokenEndpointRequestMethods(HttpMethod.POST,HttpMethod.GET)
//要配置令牌的生成与存储,由于令牌生成比较复杂,下面有方法实现
.tokenServices(tokenService());//这个不配置,默认令牌为UUID.randomUUID().toString()
}
//定义令牌生成策略
@Bean
public AuthorizationServerTokenServices tokenService(){
//这个方法的目标就是获得一个令牌生成器(此对象提供了创建、获取、刷新token的方法)
DefaultTokenServices services=new DefaultTokenServices();
//支持令牌刷新策略(令牌有过期时间)
services.setSupportRefreshToken(true);
//设置令牌生成存储策略(tokenStore在TokenConfig配置了,本次我们应用JWT-定义了一种令牌格式)
services.setTokenStore(tokenStore);
//设置令牌增强(允许设置令牌生成策略,默认是非jwt方式,没有业务数据,太简单,现在设置为jwt方式,并在令牌Payload部分允许添加扩展数据,例如用户权限信息)
TokenEnhancerChain chain=new TokenEnhancerChain();
chain.setTokenEnhancers(Arrays.asList(jwtAccessTokenConverter));//转换器
services.setTokenEnhancer(chain);//令牌增强
//设置令牌有效期
services.setAccessTokenValiditySeconds(3600);//1小时
//刷新令牌应用场景:一般在用户登录系统后,令牌快过期时,系统自动帮助用户刷新令牌,提高用户的体验感
services.setRefreshTokenValiditySeconds(3600*72);//3天
return services;
}
//给哪些客户端发送令牌,服务端规则定义
//如限制高消费的人不能坐高铁
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory()
//客户端id (客户端访问时需要这个id)
.withClient("gateway-client")
//客户端秘钥(客户端访问时需要携带这个密钥)
.secret(passwordEncoder.encode("123456"))
//设置权限
.scopes("all")//all只是个名字而已和写abc效果相同
//允许客户端进行的操作 这里的认证方式表示密码方式,里面的字符串千万不能写错
.authorizedGrantTypes("password","refresh_token");
}
// 认证成功后的安全约束配置,对指定资源的访问放行,我们登录时需要访问/oauth/token,需要对这样的url进行放行
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
//认证通过后,允许客户端进行哪些操作 参数permitAll()是官方定义好的
security
//公开oauth/token_key端点
.tokenKeyAccess("permitAll()")
//公开oauth/check_token端点
.checkTokenAccess("permitAll()")
//允许提交请求进行认证(申请令牌)
.allowFormAuthenticationForClients();
}
}
在服务端也添加TokenConfig配置令牌
用户登陆成功以后可以携带token访问服务端资源服务器,资源服务器中需要有解析token的对象
//在此配置类中配置令牌的生成,存储策略,验签方式(令牌合法性)。
@Configuration
public class TokenConfig {
//JWT令牌签名时使用的密钥(盐值)
private String SIGNING_KEY = "auth";
//配置令牌的存储策略,对于oauth2规范中提供了集中策略
@Bean
public TokenStore tokenStore() {
return new JwtTokenStore(jwtAccessTokenConverter());
}
//定义Jwt转换器,负责生成jwt令牌,解析令牌内容,及验签方式
@Bean
public JwtAccessTokenConverter jwtAccessTokenConverter(){
JwtAccessTokenConverter converter=new JwtAccessTokenConverter();
//设置加密/解密口令
converter.setSigningKey(SIGNING_KEY);
return converter;
}
}
启动和配置认证和授权规则
在服务端
@Configuration
@EnableResourceServer
@EnableGlobalMethodSecurity(prePostEnabled = true) //启动方法上的权限控制,需要授权才可访问的方法上添加@PreAuthorize等相关注解
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {//资源服务配置拦截
@Override
public void configure(HttpSecurity http) throws Exception {
http.csrf().disable();
//异常处理,可以省略
http.exceptionHandling()
.accessDeniedHandler(accessDeniedHandler());
http.authorizeRequests()
//.antMatchers("/resource/upload/**").authenticated() // 限制访问
.anyRequest().permitAll();
}
}
继承ResourceServerConfigurerAdapter类重写configure方法
ResourceController 方法配置在controller的上传方法上添加 @PreAuthorize(“hasAuthority(‘sys:res:create’)”)注解,用于告诉底层框架方法此方法需要具备的权限
@PreAuthorize("hasAuthority('sys:res:create')")
@RequiredLog("文件上传")//此注解描述的为切入点方法
@PostMapping("upload")
public String uploadFile(MultipartFile uploadFile) throws IOException {
测试
1.登录测试
2.超级用户上传权限测试
3.普通用户上传测试
总结
- 单点登陆解决方案:(市场常用两种: spring security+jwt+oauth2,spring securit+redis+oauth2)
- Spring Security 是spring框架中的一个安全默认,实现了认证和授权操作
- JWT是一种令牌格式,一种令牌规范,通过对JSON数据采用一定的编码,加密进行令牌设计
- OAuth2是一种认证和授权规范,定义了单点登陆中服务的划分方式,认证的相关类型



