微服务版单点登陆系统(SSO)实践

微服务版单点登陆系统(SSO)实践,第1张

微服务版单点登陆系统(SSO)实践 1.单点登陆系统概述

单点登录,英文是 Single Sign On(缩写为 SSO)。即多个站点共用一台认证授权服务器,用户在其中任何一个站点登录后,可以免登录访问其他所有站点。而且,各站点间可以通过该登录状态直接交互。

2.单点登陆系统解决方案设计 2.1解决方案一:用户登陆成功以后,将用户登陆状态存储到redis数据库


在这套方案中,用户登录成功后**,会基于UUID生成一个token**,然后与用户信息绑定在一起存储到数据库.后续用户在访问资源时,基于token从数据库查询用户状态,这种方式因为要基于数据库存储和查询用户状态,所以性能表现一般。

2.2解决方案2:用户登陆成功以后,将用户信息存储到token(令牌),然后写到客户端进行存储


在这套方案中,用户登录成功后,会基于JWT技术生成一个token,用户信息可以存储到这个token中.后续用户在访问资源时,对token内容解析,检查登录状态以及权限信息,无须再访问数据库。

3.单点登陆系统初步设计 3.1服务设计


服务基于业务划分,系统(system)服务只提供基础数据(例如用户信息、日志信息等),认证服务(auth)负责完成用户身份校验、密码比对,资源服务(resource)代表一些业务服务(例如我的订单、收藏等)。

3.2创建项目工程

父工程:02-sso
配置pom文件:



    4.0.0

    com.jt
    02-sso
    1.0-SNAPSHOT
    

    
    
        
            
            
                org.springframework.boot
                spring-boot-dependencies
                2.3.2.RELEASE
                pom
                import
            

            
            
                org.springframework.cloud
                spring-cloud-dependencies
                Hoxton.SR9
                pom
                import
            

            
            
                com.alibaba.cloud
                spring-cloud-alibaba-dependencies
                2.2.6.RELEASE
                pom
                import
            
        
    
    
    
        
        
            org.projectlombok
            lombok
            provided
        
        
        
            org.springframework.boot
            spring-boot-starter-test
            test
            
                
                    org.junit.jupiter
                    junit-jupiter-engine
                
            
        
        
    
    
    
        
            
            
                org.apache.maven.plugins
                maven-compiler-plugin
                
                3.8.1
                
                    8
                    8
                
            
        
    

3.3系统基础服务工程设计及实现

本次设计系统服务(System),主要用于提供基础数据服务,例如日志信息,用户信息等

3.3.1数据库表设计

3.3.2创建子工程sso-system 3.3.2.1添加依赖:



    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

3.3.2.2在项目中添加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: 123456

可将连接数据库的配置,添加到配置中心

3.3.2.3在项目中添加启动类
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);
    }
}
3.3.2.4在项目中添加单元测试类,测试数据库连接
package com.jt;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.SQLException;

@SpringBootTest
public class DataSourceTests {
    @Autowired
    private DataSource dataSource;//HikariDataSource
    @Test
    void testGetConnection() throws SQLException {
        Connection conn=
        dataSource.getConnection();
        System.out.println(conn);
    }
}
3.4POJO对象逻辑实现

添加项目User对象,用于封装用户信息

java中所有用于存储数据的对象,都建议实现序列化接口,并且添加一个序列化id
可参考:String,Integer,ArrayList,HashMap

@Data //编译时有效
@Accessors(chain = true)
//@TableName("tb_users"),假如sql语句自己写,不需要此注解指定表名
public class User implements Serializable {//此接口起标识性作用
    //序列化id
    private static final long serialVersionUID = -9218088594214708448L;
    private Long id;
    private String username;
    private String password;
    private String status;
}
实现序列化id,alt+enter自动添加UID

Service中@Autowired注入UserMapper中对象红色波浪线解决


Java中连接池设计需要遵循的数据源规范是(javax.sql.DataSource),连接池这块能想到的设计模式有(单例,享元,桥接,slf4j门面) 3.5创建UserMapper接口,并定义基于用户名查询用户信息,基于用户id查询用户权限信息的方法
@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(Long userId);
}
3.6创建UserMapperTests类,对业务方法做单元测试
@SpringBootTest
public class UserMapperTests {

    @Autowired
    private UserMapper userMapper;

    @Test
    void testSelectUserByUsername(){
        User user = userMapper.selectUserByUsername("admin");
        System.out.println(user);
        //断言测试,测试结果不正确,就抛异常
        //Assert.notNull(user, "user is not exist");
        //推荐此种方式
        Assertions.assertNotNull(user,"user is not null");
    }
    @Test
    void testSelectUserPermissions(){
        List permissions = userMapper.selectUserPermissions(1L);
        System.out.println(permissions);
        Assertions.assertNotNull(permissions,"permissions is not null");
    }
}
3.7定义service接口
public interface UserService  {
    User selectUserByUsername(String username);
    List selectUserPermissions(Long userId);
}
3.8定义service接口实现类
@Service
public class UserServiceImpl implements UserService {

    @Autowired
    private UserMapper userMapper;
    @Override
    public User selectUserByUsername(String username) {
        //Assert.notNull(username,"username can not be null");
        //判断是否为空,抛异常
        if (StringUtils.isEmpty(username)){
            throw new IllegalArgumentException("username can not be null");
        }
        return userMapper.selectUserByUsername(username);
    }

    
    @Cacheable(value = "permissionCache")
    @Override
    public List selectUserPermissions(Long userId) {
        return userMapper.selectUserPermissions(userId);
    }
}

@EnableCaching //开启spring中的缓存机制,扫描哪个类中用到了缓存
@Cacheable(value = "permissionCache"),此注解描述的方法为缓存切入点方法,从数据库查询到数据后, 可以将数据存储到本地的一个缓存对象中(底层是一个map对象)

3.9Controller对象逻辑实现
@RestController
@RequestMapping("/user")
public class UserController {

    @Autowired
    private UserService userService;

    @GetMapping("/login/{username}")
    public User doSelectUserByUsername(@PathVariable String username){
        return userService.selectUserByUsername(username);
    }
    @GetMapping("/permission/{userId}")
    public List doSelectUserPermissions(@PathVariable Long userId){
        return userService.selectUserPermissions(userId);
    }
}
4.统一认证工程设计及实现

用户登陆时调用此工程对用户身份进行统一身份认证和授权。

4.1创建工程sso-auth及初始化 4.1.1编辑pom文件
 
        
            org.springframework.boot
            spring-boot-starter-web
        
        
            com.alibaba.cloud
            spring-cloud-starter-alibaba-nacos-discovery
        
        
            com.alibaba.cloud
            spring-cloud-starter-alibaba-nacos-config
        
        
        
            org.springframework.cloud
            spring-cloud-starter-oauth2
        
        
        
            org.springframework.cloud
            spring-cloud-starter-openfeign
        
    
4.1.2在sso-auth工程中创建bootstrap.yml文件
server:
  port: 8071
spring:
  application:
    name: sso-auth
  cloud:
    nacos:
      discovery:
        server-addr: localhost:8848
      config:
        server-addr: localhost:8848
4.1.3添加项目启动类
@SpringBootApplication
public class AuthApplication {
    public static void main(String[] args) {
        SpringApplication.run(AuthApplication.class, args);
    }
}
4.1.4启动并访问项目

项目启动时,系统会默认生成一个登陆密码

打开浏览器输入http://localhost:8071呈现登录页面
默认用户名为user,密码为系统启动时,在控制台呈现的密码。执行登陆测试,登陆成功进入如下界面(因为没有定义登录页面,所以会出现404)。

4.2定义用户信息处理对象

第一步:定义User对象,用于封装从数据库查询到的用户信息

package com.jt.auth.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;
}

第二步:定义用户远程调用Feign接口RemoteUserService ,基于此接口调用sso-system服务中的用户信息

@FeignClient(value = "sso-system",
             contextId = "remoteUserService")
public interface RemoteUserService {

    @GetMapping("/user/login/{username}")
    User selectUserByUsername(@PathVariable(name = "username") String username);

    
    @GetMapping("/user/permission/{userId}")
    List selectUserPermissions(@PathVariable("userId") Long userId);
}

第三步:定义远程Service对象,用于实现远程用户信息调用

@Slf4j
@Service
public class UserDetailServiceImpl implements UserDetailsService {
    @Autowired
    private RemoteUserService remoteUserService;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        //1.基于用户名获取用户信息(远程Feign方式服务调用)
        com.jt.auth.pojo.User user = remoteUserService.selectUserByUsername(username);
        if(user == null)
            throw new UsernameNotFoundException("user id not exist");
        //2.基于用户id获取用户权限信息
        List permissions = remoteUserService.selectUserPermissions(user.getId());
        log.debug("permissions {}",permissions);
        //3.封装用户信息并返回
        User userDetails = new User(username,
                user.getPassword(),
                AuthorityUtils.createAuthorityList(permissions.toArray(new String[]{})));
        return userDetails; //交给spring security的认证中心,进行认证分析(比对)
    }
}
5.定义Security配置类

在此类中配置认证规则

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    
    @Bean
    public PasswordEncoder passwordEncoder() {

        return new BCryptPasswordEncoder();
    }
    
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        //super.configure(http);
        //禁用跨域攻击,假如没有禁用,使用postman,httpclient这些工具登录失败403
        //http.csrf().disable();
        //所有**资源必须认证才能访问,403没有权限
        //http.authorizeRequests().antMatchers("
        //登录配置,去哪里认证,认证成功或失败的处理器是谁
//        http.formLogin().defaultSuccessUrl("index.html"); //redirect:index.html重定向
//        http.formLogin().successForwardUrl("/doIndex");

        //前后端分离的写法,登录成功要返回json字符串
        http.formLogin()
                .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("status", 200);
                map.put("message", "login success");
                writeJsonToClient(response,map);
            }
        };
    }
    //lambda
    public AuthenticationFailureHandler failureHandler() {
        return (request, response, authentication) ->{
                Map map = new HashMap<>();
                map.put("status", 201);
                map.put("message", "login failure");
                writeJsonToClient(response,map);
        };
    }
    private void writeJsonToClient(HttpServletResponse response,Map map) throws IOException {
        //将map对象,转换为json
        String jsonStr =new ObjectMapper().writevalueAsString(map);
        //设置响应数据的编码方式
        response.setCharacterEncoding("utf-8");
        //设置响应数据的类型
        response.setContentType("application/json;charset:utf8");
        //将数据响应到客户端
        PrintWriter out=response.getWriter();
        out.println(jsonStr);
        out.flush();
    }
}
基于浏览器进行访问测试

启动sso-system,sso-auth服务,然后基于浏览器访问网关,执行登录测试,admin,123456

6.Security 认证流程分析

目前的登陆 *** 作,也就是用户的认证 *** 作,其实现主要基于Spring Security框架,其认证简易流程如下

6.1定义Oauth2认证授权配置

Oauth2是一种协议或规范,定义了完成用户身份认证和授权的方式,
比如:基于密码身份认证,基于指纹,基于第三方令牌认证(QQ,微信登录),但具体完成过程需要一组对象,这些对象构成,有如下几个部分:
0.系统数据资源(类似数据库,文件系统)
1.资源服务器(负责访问资源,例如:商品,订单,库存,会员…)
2.认证服务器(负责完成用户身份的认证)
3.客户端对象(表单,令牌,)
4.资源拥有者(用户)

在Oauth2这中规范下,如何对用户什么进行认证?
1.认证的地址(让用户去哪里认证)
2.用户需要携带什么信息去认证(办理)
3.具体完成认证的对象是谁

在Security配置类添加以下方法,在Oauth2Config注入此对象:

//方法返回的对象为后续的oauth2的配置提供服务
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
    return super.authenticationManagerBean();
}

定义Oauth2Config配置类:

@Configuration
@EnableAuthorizationServer
public class Oauth2Config extends AuthorizationServerConfigurerAdapter {

    @Autowired
    private PasswordEncoder passwordEncoder;
    @Autowired
    private AuthenticationManager authenticationManager;
    //提供一个认证的入口(客户端去哪里认证)?(http://ip:port/.....)

    
    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
        //super.configure(security);
        //公开认证地址(/auth/token)
        security.tokenKeyAccess("permitAll()")
                //公开检查令牌的入口(/oauth/check_token)
                .checkTokenAccess("permitAll()")
                //允许通过表单方式进行认证
                .allowFormAuthenticationForClients();
    }

    
    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        //super.configure(clients);
        clients.inMemory()
                //客户端标识
                .withClient("gateway-client")
                //客户端携带密钥 123456
                .secret(passwordEncoder.encode("123456"))
                //定义认证类型(允许对哪些数据进行认证)
                .authorizedGrantTypes("password","refresh_token")
                //作用域,满足如上条件的所有客户端可以来这里进行认证
                .scopes("all");

    }

    
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        //super.configure(endpoints);
        //定义由谁完成认证
        endpoints.authenticationManager(authenticationManager);
    }
}
6.2构建令牌生成及配置对象

本次我们借助JWT(Json Web Token-是一种json格式)方式将用户相关信息进行组织和加密,并作为响应令牌(Token),从服务端响应到客户端,客户端接收到这个JWT令牌之后,将其保存在客户端(例如localStorage),然后携带令牌访问资源服务器,资源服务器获取并解析令牌的合法性,基于解析结果判定是否允许用户访问资源。

@Configuration
public class TokenConfig {
    
    @Bean
    public TokenStore tokenStore() {
        return new JwtTokenStore(jwtAccessTokenConverter());
    }

    @Bean
    public JwtAccessTokenConverter jwtAccessTokenConverter(){
        JwtAccessTokenConverter jwtAccessTokenConverter = new JwtAccessTokenConverter();
        //设置签名key,对Jwt令牌进行签名时使用,key不能让客户端知道
        jwtAccessTokenConverter.setSigningKey(signingKey);
        return jwtAccessTokenConverter;
    }

    
    private String signingKey="auth";
}
6.3启动postman进行访问测试


检查token信息:

7.资源服务工程设计及实现

资源服务工程为一个业务数据工程,此工程中数据在访问通常情况下是受限访问,例如有些资源有用户,都可以方法,有些资源必须认证才可访问,有些资源认证后,有权限才可以访问。

7.1业务设计架构

用户访问资源时的认证,授权流程设计如下:

7.2创建sso-resource工程 7.2.1第一步:初始化pom文件依赖

        
        
            org.springframework.boot
            spring-boot-starter-web
        
        
        
            com.alibaba.cloud
            spring-cloud-starter-alibaba-nacos-discovery
        
        
        
            com.alibaba.cloud
            spring-cloud-starter-alibaba-nacos-config
        
        
        
            com.alibaba.cloud
            spring-cloud-starter-alibaba-sentinel
        

        
        
            org.springframework.cloud
            spring-cloud-starter-oauth2
        
    
7.2.2第二步:创建bootstrap.yml配置文件:
server:
  port: 8881
spring:
  application:
    name: sso-resource
  cloud:
    nacos:
      discovery:
        server-addr: localhost:8848
      config:
        server-addr: localhost:8848
        file-extension: yml
7.2.3第三步:创建启动类
package com.jt;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class ResourceApplication {
    public static void main(String[] args) {
        SpringApplication.run(ResourceApplication.class,args);
    }
}
7.2.4第四步:创建资源Controller对象
@RestController
@RequestMapping("/resource")
public class ResourceController {

    
    @PreAuthorize("hasAuthority('sys:res:list')")//描述的是一个切入点方法,访问此方法,需加此权限
    @GetMapping
    public String doSelect(){
        return "Select Resource ok";
    }
    
    @PreAuthorize("hasAuthority('sys:res:create')")
    @PostMapping
    public String doCreate(){
        return "Create Resource OK";
    }
    
    @PreAuthorize("hasAuthority('sys:res:update')")
    @PutMapping
    public String doUpdate(){
        return "Update Resource OK";
    }
    
    @DeleteMapping
    public String doDelete(){
        return "Delete resource ok";
    }
}
7.2.5第五步:配置资源认证授权规则ResourceConfig
@Configuration
@EnableResourceServer //启动资源服务器的默认配置
//启动方法上的权限控制,需要授权才可访问的方法上添加@PreAuthorize等相关注解
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class ResourceConfig extends ResourceServerConfigurerAdapter {

    @Override
    public void configure(HttpSecurity http) throws Exception {
        //super.configure(http);
        //1.关闭跨域攻击
        http.csrf().disable();
        //2.放行相关请求
        http.authorizeRequests()
                //配置炫耀认证的资源
                .antMatchers("/resource
@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";
}
7.2.7第七步:启动Postman进行访问测试:

不携带令牌访问:

携带令牌访问:

没有访问权限:

欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/zaji/5693613.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-12-17
下一篇 2022-12-17

发表评论

登录后才能评论

评论列表(0条)

保存