概述:
单点登录,英文是 Single Sign On(缩写为 SSO)。即多个站点共用一台认证授权服务器,用户在其中任何一个站点登录后,可以免登录访问其他所有站点。而且,各站点间可以通过该登录状态直接交互。那为什么要用单点登录呢?为了在分布式系统中让用户再访问不同服务资源时,不需要进行多次登录,进而改善用户的体验。如何解决呢?
解决方案:根据了解常用两种: spring security+jwt+oauth2,spring security+redis+oauth2,那么本次就采用两个中其spring security+jwt+oauth2这一方式进行实现,使用户登陆成功以后,将用户信息存储到token(令牌),然后写到客户端进行存储,本次令牌采用JWT的格式进行存储。
工程结构设计
创建父类工程02-sso,在父类工程下创建两个子工程,一个sso-system子工程用于存放用户信息,另一子工程sso-auth用于对用户的身份进行统一的身份认证和授权。
创建父工程
第一步:创建父工程,例如:
初始化pom文件内容,例如:
4.0.0 com.jt 02-ssopom 1.0-SNAPSHOT sso-system org.springframework.boot spring-boot-dependencies2.3.2.RELEASE pom import org.springframework.cloud spring-cloud-dependenciesHoxton.SR9 pom import com.alibaba.cloud spring-cloud-alibaba-dependencies2.2.6.RELEASE pom import org.projectlombok lombokprovided org.springframework.boot spring-boot-starter-testtest org.junit.jupiter junit-vintage-engineorg.apache.maven.plugins maven-compiler-plugin3.8.1 8 8
表结构设计
创建sso-system子工程
这一工程主要用于提供基础数据服务。
工程结构:
创建工程添加项目依赖,例如
mysql
mysql-connector-java
com.baomidou
mybatis-plus-boot-starter
3.4.2
com.alibaba.cloud
spring-cloud-starter-alibaba-nacos-discovery
com.alibaba.cloud
spring-cloud-starter-alibaba-nacos-config
org.springframework.boot
spring-boot-starter-web
在项目中添加bootstrap.yml文件,其内容如下:
server:
port: 8061
spring:
application:
name: sso-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
也可以把数据库的连接写入配置中心,注意写这个文件时候注意书写时的缩进,避免不必要的麻烦。
添加启动类
package com.jt;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class SystemApplication {
public static void main(String[] args) {
SpringApplication.run(SystemApplication.class,args);
}
}
Pojo对象逻辑实现:
添加User类,用于封装用户的信息。
package com.jt.system.pojo;
import lombok.Data;
import java.io.Serializable;
@Data
public class User implements Serializable {
private static final long serialVersionUID = 4831304712151465443L;
private Long id;
private String username;
private String password;
private String status;
}
注意:介于存取时我们都要实现序列化接口。
Dao对象逻辑实现:
创建UserMapper接口,并定义基于用户名查询用户信息,基于用户id查询用户权限信息的方法,代码如下:
package com.jt.system.dao; import com.baomidou.mybatisplus.core.mapper.baseMapper; import com.jt.system.pojo.User; import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Select; import java.util.List; @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 permission from tb_user_roles an" + "join tb_role_menus b on a.role_id = b.role_idn" + "join tb_menus c on b.menu_id = c.id n" + "where a.id=#{userId}") List selectUserPermissions(Long userId); }
Service对象的逻辑实现
第一步:定义service接口,代码如下:
package com.jt.system.service;
import com.jt.system.pojo.User;
import java.util.List;
public interface UserService {
User selectUserByUsername(String username);
List selectUserPermissions(Long userId);
}
第二步:定义service接口实现类,代码如下:
package com.jt.system.service.impl;
import com.jt.system.dao.UserMapper;
import com.jt.system.pojo.User;
import com.jt.system.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class UserServiceImpl implements UserService {
@Autowired
private UserMapper userMapper;
@Override
public User selectUserByUsername(String username) {
return userMapper.selectUserByUsername(username);
}
@Override
public List selectUserPermissions(Long userId) {
return userMapper.selectUserPermissions(userId);
}
}
Controller对象逻辑实现
package com.jt.system.controller;
import com.jt.system.pojo.User;
import com.jt.system.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
@RestController
@RequestMapping("/user/")
public class UserController {
@Autowired
private UserService userService;
@RequestMapping("/login/{username}")
public User doSelectUserByUsername(
@PathVariable("username") String username){
return userService.selectUserByUsername(username);
}
@RequestMapping("/permission/{userId}")
public List doSelectUserPermissions(
@PathVariable("userId") Long userId){
return userService.selectUserPermissions(userId);
}
}
启动服务进行测试看看是否能查询到对应的信息:
测试用户基本信息查询到之后进行统一认证工程的设计。
统一认证工程设计
创建sso-auth子工程
此工程对用户身份进行统一身份认证和授权。
工程结构:
第一步:创建sso-auth工程
第二步:打开sso-auth工程中的pom文件,然后添加如下依赖:
org.springframework.boot spring-boot-starter-webcom.alibaba.cloud spring-cloud-starter-alibaba-nacos-discoverycom.alibaba.cloud spring-cloud-starter-alibaba-nacos-configorg.springframework.cloud spring-cloud-starter-oauth2org.springframework.cloud spring-cloud-starter-openfeign
第三步:在sso-auth工程中创建bootstrap.yml文件,例如:
server:
port: 8071
spring:
application:
name: sso-auth
cloud:
nacos:
discovery:
server-addr: localhost:8848
config:
server-addr: localhost:8848
第四步 添加项目启动类,因为此项目要基于fein方式用到之前的sso-system,所以要在启动类上加上@EnableFeignClients
package com.jt;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.openfeign.EnableFeignClients;
@EnableFeignClients
@SpringBootApplication
public class AuthApplication {
public static void main(String[] args) {
SpringApplication.run(AuthApplication.class, args);
}
}
启动并访问项目
项目启动时,系统会默认生成一个登陆密码,例如:
可以拿着获取到的默认密码测试访问
默认用户名为user,密码为系统启动是控制台上显示的密码登录成功进去显示404(因为没有定义登陆页面)
定义用户信息处理对象
第一步:定义User对象,用于封装从数据库查询到的用户信息同上。
第二步:定义远程Service对象,用于实现远程用户信息调用,例如:
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;
@FeignClient(value = "sso-system",contextId = "remoteUserService")
public interface RemoteUserService {
@GetMapping("/user/login/{username}")
User selectUserByUsername(
@PathVariable("username") String username);
@GetMapping("/user/permission/{userId}")
List 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.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;
import java.util.List;
@Slf4j
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
private RemoteUserService remoteUserService;
@Override
public UserDetails loadUserByUsername(String username)
throws UsernameNotFoundException {
//基于feign方式获取远程数据并封装
//1.基于用户名获取用户信息
com.jt.auth.pojo.User user=
remoteUserService.selectUserByUsername(username);
if(user==null)
throw new UsernameNotFoundException("用户不存在");
//2.基于用于id查询用户权限
List permissions=
remoteUserService.selectUserPermissions(user.getId());
log.info("permissions {}",permissions);
//3.对查询结果进行封装并返回
User userInfo= new User(username,
user.getPassword(),
AuthorityUtils.createAuthorityList(permissions.toArray(new String[]{})));
//......
return userInfo;
//返回给认证中心,认证中心会基于用户输入的密码以及数据库的密码做一个比对
}
}
定义Security配置类
此类用于配置认证的一些规则,登录成功/失败返回客户端的信息,及资源访问时的认证与放行。
package com.jt.auth.config;
import org.codehaus.jackson.map.ObjectMapper;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.HashMap;
import java.util.Map;
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Bean
public BCryptPasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
@Bean
public AuthenticationManager authenticationManagerBean()
throws Exception {
return super.authenticationManager();
}
@Override
protected void configure(HttpSecurity http)
throws Exception {
//super.configure(http);//默认所有请求都要认证
//1.禁用跨域攻击(先这么写,不写的话在postman和idea中测试会报403异常)
http.csrf().disable();
//2.放行所有资源的访问(后续可以基于选择对资源进行认证和放行)
http.authorizeRequests()
.anyRequest().permitAll();//所有都不需要认证
//3.设置认证结果处理器(默认认证成功会跳转到自己设置的index.html)自定义定义登录成功和失败以后的处理逻辑(可选)
//假如没有如下设置登录成功会显示404
http.formLogin()//方法执行后会创建一个/login路径
.successHandler(successHandler()).failureHandler(failureHandler());
}
//定义认证成功处理器
//登录成功以后返回json数据
@Bean
public AuthenticationSuccessHandler successHandler(){
return new AuthenticationSuccessHandler() {
@Override
public void onAuthenticationSuccess(
HttpServletRequest request,
HttpServletResponse response,
Authentication authentication) throws IOException, ServletException {
Map map=new HashMap<>();
map.put("state",200);
map.put("message", "login ok");
//将map对象转换为json格式字符串并写到客户端
writeJsonToClient(response,map);
}
};
}
//定义登录失败处理器
@Bean
public AuthenticationFailureHandler failureHandler(){
return new AuthenticationFailureHandler() {
@Override
public void onAuthenticationFailure(
HttpServletRequest request,
HttpServletResponse response,
AuthenticationException e) throws IOException, ServletException {
Map map=new HashMap<>();
map.put("state",500);
map.put("message", "login error");
//将map对象转换为json格式字符串并写到客户端
writeJsonToClient(response,map);
}
};
}
private void writeJsonToClient(
HttpServletResponse response,
Map map) throws IOException {
//将map对象,转换为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();
}
}
测试:
Security认证流程分析:
Spring Security是一个安全认证的框架:这个执行链底层框架已经设计好,我们只需要执行,详细也可debug跟下具体流程。
1)客户端提交用户名和密码给服务端
2)服务端调用Spring Security框架中的过滤器(Filters)对用户名和密码进行预处理
3)过滤器(Filters)将用户名和密码传递给认证管理器(AuthenticationManager)完成用户身份认证
4)认证管理器会调用UserDetailsService对象获取远端服务或数据库中的用户信息,然后
与客户端提交的用户信息进行比对(这个比对过程就是认证)。
5)认证通过则基于用户权限对用户进行资源访问授权。
单体架构中的用户的状态存储实现(默认是存储到了toncat的session对象中)
定义Oauth2认证授权配置
此类用于配置令牌的格式及令牌存储的格式本次使用的时JWT格式和令牌的有效时长。
package com.jt.auth.config;
import lombok.AllArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
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.*;
import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;
@AllArgsConstructor //生成一个全参构造函数
@Configuration
@EnableAuthorizationServer//启动认证和授权
public class Oauth2Config extends AuthorizationServerConfigurerAdapter {
private AuthenticationManager authenticationManager;
private UserDetailsService userDetailsService;
private TokenStore tokenStore;
private BCryptPasswordEncoder passwordEncoder;
private JwtAccessTokenConverter jwtAccessTokenConverter;
@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());
}
//系统底层在完成认证以后会调用TokenService对象的相关方法
//获取TokenStore,基于tokenStore获取token对象
@Bean
public AuthorizationServerTokenServices tokenService(){
//1.构建TokenService对象(此对象提供了创建,获取,刷新token的方法)
DefaultTokenServices tokenServices=new DefaultTokenServices();
//2.设置令牌生成和存储策略
tokenServices.setTokenStore(tokenStore);
//3.设置是否支持令牌刷新(访问令牌过期了,是否支持通过令牌刷新机制,延长令牌有效期)
tokenServices.setSupportRefreshToken(true);
//4.设置令牌增强(默认令牌会比较简单,没有业务数据,
//就是简单随机字符串,但现在希望使用jwt方式)
tokenServices.setTokenEnhancer(jwtAccessTokenConverter);
//5.设置访问令牌有效期
tokenServices.setAccessTokenValiditySeconds(3600);//1小时
//6.设置刷新令牌有效期
tokenServices.setRefreshTokenValiditySeconds(3600*72);//3天
return tokenServices;
}
@Override
public void configure(AuthorizationServerSecurityConfigurer security)
throws Exception {
security
//1.定义(公开)要认证的入口url(permitAll()是官方定义好的)
//公开oauth/token_key端点
.tokenKeyAccess("permitAll()") //return this
//2.定义(公开)检查令牌的入口url
//公开oauth/check_token端点
.checkTokenAccess("permitAll()")
//3.允许客户端直接通过表单方式提交认证,完成认证
.allowFormAuthenticationForClients();
}
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
//super.configure(clients);
clients.inMemory()
//定义客户端的id(客户端提交用户信息进行认证时需要这个id)
.withClient("gateway-client")
//定义客户端密钥(客户端提交用户信息时需要携带这个密钥)
.secret(passwordEncoder.encode("123456"))
//定义作用范围(所有符合规则的客户端)
.scopes("all")
//允许客户端基于密码方式,刷新令牌方式实现认证
.authorizedGrantTypes("password","refresh_token");
}
}
定义Token类
此类为了构建令牌生成及配置令牌存储格式JWT以及验证方式时基于Oauth2所需把共性的配置可以提到一块
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 {
@Bean
public TokenStore tokenStore(){
//这里采用JWT方式生成和存储令牌信息
return new JwtTokenStore(jwtAccessTokenConverter());
}
@Bean
public JwtAccessTokenConverter jwtAccessTokenConverter(){
JwtAccessTokenConverter jwtAccessTokenConverter=
new JwtAccessTokenConverter();
//JWT令牌构成:header(签名算法,令牌类型),payload(数据部分),Signing(签名)
//这里的签名可以简单理解为加密,加密时会使用header中算法以及我们自己提供的密钥,
//这里加密的目的是为了防止令牌被篡改。(这里密钥要保管好,要存储在服务端)
jwtAccessTokenConverter.setSigningKey(SIGNING_KEY);//设置密钥
return jwtAccessTokenConverter;
}
private static final String SIGNING_KEY="auth";
}
最后启动Postman进行测试检查令牌是否创建成功或者也可以创建两个静态页面进行测试,看看是否成功,根据认证规则的代码自己设置认证规则,测试访问哪个访问时需要认证,那个不需要认证。
检查令牌创建是否成功时url端口号后一定要跟/oauth/token,成功会显示以下信息:
在测试刷新令牌时可以将最初产生的令牌最为他的value传进去:
以上spring security+jwt+oauth2这一方式的登录实现就可以让用户再访问不同服务资源时,不需要进行多次登录了。继续加油,冲!!!



