1. 背景

多点登录,访问每个服务都需要重新登陆

2. 概念

登录一个站点后可以免登录其他站点

3. 准备工作

3.1 项目结构

gateway 微服务登录验证 微服务登陆_spring

3.2 执行sql脚本

gateway 微服务登录验证 微服务登陆_User_02

3.3 数据库表结构

gateway 微服务登录验证 微服务登陆_微服务_03


gateway 微服务登录验证 微服务登陆_microservices_04


gateway 微服务登录验证 微服务登陆_microservices_05


gateway 微服务登录验证 微服务登陆_User_06


gateway 微服务登录验证 微服务登陆_gateway 微服务登录验证_07


gateway 微服务登录验证 微服务登陆_spring_08


gateway 微服务登录验证 微服务登陆_User_09

3.4 sso-system子工程的依赖

<dependencies>
        <!--1.数据库访问相关-->
        <!--1.1 mysql 数据库驱动-->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
        <!--1.2 mybatis plus 插件-->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.4.2</version>
        </dependency>
        <!--服务治理相关-->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
        </dependency>
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
        </dependency>
        <!--Web 服务相关-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
    </dependencies>

3.5 sso-system子工程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
logging: #配置日志级别
  level: 
    com.jt: debug

3.6 测试连接

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);
    }
}

实现效果

gateway 微服务登录验证 微服务登陆_microservices_10

4. 业务1

4.1 编辑pojo

package com.jt.system.pojo;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;

import java.io.Serializable;

@Data
@TableName("tb-users")
public class User implements Serializable {
    private static final long serialVersionUID = 4831304712151465443L;
    @TableId(type = IdType.AUTO)
    private Long id;
    private String username;
    private String password;
    private String status;

}

gateway 微服务登录验证 微服务登陆_microservices_11


gateway 微服务登录验证 微服务登陆_gateway 微服务登录验证_12


gateway 微服务登录验证 微服务登陆_microservices_13

4.2 编辑Dao层

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<User> {
    @Select("select id,username,password,status " +
            "from tb_users " +
            "where username=#{username}")
    User selectUserByUsername(String username);
  
}

4.3 编辑测试类

package com.jt;
import com.jt.system.pojo.User;
import com.jt.system.dao.UserMapper;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import java.util.List;
@SpringBootTest
public class UserMapperTests {
    @Autowired
    private UserMapper userMapper;
    @Test
    void testSelectUserByUsername(){
        User user =
                userMapper.selectUserByUsername("admin");
        System.out.println(user);
    }
}

实现效果

gateway 微服务登录验证 微服务登陆_User_14

拓展:享元模式

5. 业务2基于用户id查询用户权限

5.1 sql语句

gateway 微服务登录验证 微服务登陆_微服务_15

5.2 Mapper层

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<User> {
    @Select("select id,username,password,status " +
            "from tb_users " +
            "where username=#{username}")
    User selectUserByUsername(String username);
    /**
     * 基于用户id查询用户权限
     * @param userId 用户id
     * @return 用户的权限
     * 涉及到的表:tb_user_roles,tb_role_menus,tb_menus
     */
    @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<String> selectUserPermissions(Long userId);
}

5.3 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<String> selectUserPermissions(long userId) {
        //方案一:在这里可以调用数据层的单表查询方法,查询三次获取用户信息
        //方案二:调用数据层的多表嵌套或多表关联关联方法执行1次查询
        return userMapper.selectUserPermissions(userId);
    }
}
package com.jt.system.service;
import com.jt.system.pojo.User;
import java.util.List;
public interface UserService {
    User selectUserByUsername(String username);
    List<String> selectUserPermissions(long userId);
}

5.4 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.GetMapping;
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;
    @GetMapping("/selectUserByUsername/{username}")
    public User selectUserByUsername(@PathVariable("username") String username){
        return userService.selectUserByUsername(username);
    }
    @GetMapping("/selectUserPermissions/{userId}")
    public List<String> selectUserPermissions(@PathVariable("userId") long userId){
        return userService.selectUserPermissions(userId);
    }
}

实现效果

gateway 微服务登录验证 微服务登陆_microservices_16


gateway 微服务登录验证 微服务登陆_microservices_17


gateway 微服务登录验证 微服务登陆_User_18

5.5 配置中心配置信息

gateway 微服务登录验证 微服务登陆_microservices_19

6. 统一认证工程设计及实现

6.1 创建sso-auth子工程

gateway 微服务登录验证 微服务登陆_User_20

6.2 配置pom.xml文件

<dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
        </dependency>
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
        </dependency>
        <!--SSO技术方案:SpringSecurity+JWT+oauth2-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-oauth2</artifactId>
        </dependency>
        <!--open feign-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>
    </dependencies>

6.3 配置bootstrap.yml文件

server:
  port: 8071
spring:
  application:
    name: sso-auth
  cloud:
    nacos:
      discovery:
        server-addr: localhost:8848
      config:
        server-addr: localhost:8848

6.4 创建启动类

package com.jt;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class AuthApplication {
    public static void main(String[] args) {
        SpringApplication.run(AuthApplication.class,args);
    }
}

实现效果

为spring security内置的登陆页面

访问http://localhost:8071为什么跳到了登陆页面?

因为需要认证

gateway 微服务登录验证 微服务登陆_gateway 微服务登录验证_21

6.5 定义pojo类

package com.jt.auth.pojo;
import lombok.Data;
import java.io.Serializable;
@Data
public class User implements Serializable {
    private Long id;
    private String username;
    private String password;
    private String status;
}

6.6 基于feign调用sso-system服务

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" )
public interface RemoteUserService {
    @GetMapping("/user/login/{username}")
    User selectUserByUsername( @PathVariable("username") String username);
    @GetMapping("/user/permission/{userId}")
    List<String> selectUserPermissions(@PathVariable("userId") Long userId);
}

6.7 定义用户登陆业务逻辑处理对象

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;
/*在此对象中实现远端服务的调用,从sso-system中获取用户信息,
并对用户信息进行封装并返回给认证管理器,去完成密码的比对操作*/
@Slf4j
@Service
public class UserDetailServiceImpl implements UserDetailsService {
    @Autowired
    private RemoteUserService remoteUserService;
    /**
     * 基于用户名获取数据库中的用户信息
     * @param username 这个username来自客户端
     * @return
     * @throws UsernameNotFoundException
     */
    @Override/*我们执行登录操作的时候提交登录按钮,系统会调用此方法
                username来自客户提交的用户名
                封装了登录用户信息以及用户权限信息的一个对象
                返回UserDetails交给认证管理器*/
    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<String> 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;
        //返回给认证中心,认证中心会基于用户输入的密码以及数据库的密码做一个比对
    }
}

6.8 创建SecurityConfig配置类

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.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.HashMap;
import java.util.Map;

/**
 * 当我们在执行登录操作时,底层逻辑(了解):
 * 1)Filter(过滤器)
 * 2)AuthenticationManager (认证管理器)
 * 3)AuthenticationProvider(认证服务处理器)
 * 4)UserDetailsService(负责用户信息的获取及封装)
 */
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    /**
     * 初始化加密对象
     * 此对象提供了一种不可逆的加密方式,相对于md5方式会更加安全
     * @return
     */
    @Bean
    public BCryptPasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }

    /**
     * 定义认证管理器对象,这个对象负责完成用户信息的认证,
     * 即判定用户身份信息的合法性,在基于oauth2协议完成认
     * 证时,需要此对象,所以这里讲此对象拿出来交给spring管理
     * @return
     * @throws Exception
     */
    @Bean
    public AuthenticationManager authenticationManagerBean()
            throws Exception {
        return super.authenticationManager();
    }

    /**配置认证规则*/
    @Override
    protected void configure(HttpSecurity http)
            throws Exception {
        //super.configure(http);//默认所有请求都要认证
        //1.禁用跨域攻击(先这么写,不写会报403异常) 这里的登录是post请求 但系统底层的跨域设计是不允许post请求的
        http.csrf().disable();
        //2.放行所有资源的访问(后续可以基于选择对资源进行认证和放行)
        http.authorizeRequests()
                .anyRequest().permitAll();
        //3.自定义定义登录成功和失败以后的处理逻辑(可选)
        //假如没有如下设置登录成功会显示404
        http.formLogin()//这句话会对外暴露一个登录路径/login
                .successHandler(successHandler())
                .failureHandler(failureHandler());
    }
    //定义认证成功处理器
    //登录成功以后返回json数据
    @Bean
    public AuthenticationSuccessHandler successHandler(){
        //lambda
        return (request,response,authentication)->{
            //构建map对象封装到要响应到客户端的数据
            Map<String,Object> map=new HashMap<>();
            map.put("state",200);
            map.put("message", "login ok");
            //将map对象转换为json格式字符串并写到客户端
            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 {
        //将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();
    }
}

实现效果

gateway 微服务登录验证 微服务登陆_gateway 微服务登录验证_22

6.9 构建令牌生成及配置对象

借助JWT(Json Web Token-是一种json格式)方式将用户相关信息进行组织和加密,并作为响应令牌(Token)

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;
/*构建令牌配置对象,在微服务架构中登陆成功可以将用户信息进行存储,常用存储方式?
* 1.产生随机的字符串(token),基于此字符串将用户信息存储到关系数据库(例如mysql)
* 2.产生随机的字符串(token),基于此字符串将用户信息存储到内存数据库(例如redis) 快
* 3.基于JWT创建令牌(token),在此令牌中存储用户信息,此信息不用写到数据库,在客户端存储即可
* 基于如上存储方案,oauth2协议给出了具体的API实现对象,例如
* 1.JdbcTokenStore 用的很少
* 2.RedisTokenStore 中型应用
* 3.JwtTokenStore  对性能要求比较高的分布式架构
* */
@Configuration
public class TokenConfig {
    @Bean/*定义令牌存储方案,本次选择基于Jwt令牌方式存储用户状态*/
    public TokenStore tokenStore(){
        return new JwtTokenStore(jwtAccessTokenConverter());
    }
    @Bean/*配置jwt令牌创建和解析对象*/
    public JwtAccessTokenConverter jwtAccessTokenConverter(){
        JwtAccessTokenConverter converter=new JwtAccessTokenConverter();
        converter.setSigningKey(SIGNING_KEY);
        return converter;
    }
    //这里的签名key将来可以写到配置中心
    private static final String SIGNING_KEY="auth";
}

6.10 定义Oauth2认证授权配置

  • 配置认证规则→配置令牌→对谁颁发令牌→定义颁发令牌的路径,解析令牌的路径,校验令牌的路径
package com.jt.auth.config;

import lombok.AllArgsConstructor;
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.password.PasswordEncoder;
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.AuthorizationServerTokenServices;
import org.springframework.security.oauth2.provider.token.DefaultTokenServices;
import org.springframework.security.oauth2.provider.token.TokenEnhancerChain;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;

import java.util.Arrays;

@Configuration
@EnableAuthorizationServer
@AllArgsConstructor/*生成构造函数 为成员变量赋值*/
/*令牌存储和生成的应用方*/
public class Oauth2Config extends AuthorizationServerConfigurerAdapter {//适配器模式0

    private AuthenticationManager authenticationManager;
    private UserDetailsService userDetailsService;
    private TokenStore tokenStore;
    private PasswordEncoder passwordEncoder;
    private JwtAccessTokenConverter jwtAccessTokenConverter;
/*生成构造函数为成员变量赋值 但是加了@AllArgsConstructor即可省略*/
//    public Oauth2Config(AuthenticationManager authenticationManager,
//                        UserDetailsService userDetailsService,
//                        TokenStore tokenStore,
//                        PasswordEncoder passwordEncoder,
//                        JwtAccessTokenConverter jwtAccessTokenConverter) {
//        this.authenticationManager = authenticationManager;
//        this.userDetailsService = userDetailsService;
//        this.tokenStore = tokenStore;
//        this.passwordEncoder = passwordEncoder;
//        this.jwtAccessTokenConverter = jwtAccessTokenConverter;
//    }

    @Override/*配置认证规则*/
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
//        super.configure(endpoints);
        //配置由谁完成认证 认证管理器
        endpoints.authenticationManager(authenticationManager)
                //配置由谁负责查询用户业务数据
                  .userDetailsService(userDetailsService)
                //处理认证请求的方式  默认只能处理post请求
                  .allowedTokenEndpointRequestMethods(HttpMethod.GET,HttpMethod.POST)
                //配置token生成及存储策略 默认为uuid
                  .tokenServices(tokenServices());
    }
    @Bean
    public AuthorizationServerTokenServices tokenServices(){
        //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);//1h
        //5.设置是否支持令牌刷新(是否支持使用刷新令牌再生成新令牌)
        tokenServices.setSupportRefreshToken(true);
        //6.设置刷新令牌有效时常
        tokenServices.setRefreshTokenValiditySeconds(3600*5);
        return tokenServices;
    }
    //1.对谁颁发令牌?(对客户端的要求,如何配置)
    //2.访问哪个路径颁发令牌,需要对外暴露认证路径(定义颁发令牌的路径,解析令牌的路径,校验令牌的路径)
    /**
     * 假如我们要做认证,我们输入了用户名和密码,然后点提交
     * ,提交到哪里(url-去哪认证),这个路径是否需要认证?还有令牌过期了,
     * 我们要重新生成一个令牌,哪个路径可以帮我们重新生成?
     * 如下这个方法就可以提供这个配置  配置对外暴露的url(认证、刷新令牌、检查令牌)
     * @param security
     * @throws Exception
     */
    @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();
    }

    /**
     * 认证中心是否要给所有的客户端发令牌呢?假如不是,那要给哪些客户端 客户端有何特点
     * 发令牌,是否在服务端有一些规则的定义呢?
     * 例如:老赖不能做飞机,不能做高铁
     * @param clients 定义客户端的配置详情
     * @throws Exception
     */
    @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信息
  • 刷新令牌应用测试

    bug集

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

7.1 创建工程sso-resource

gateway 微服务登录验证 微服务登陆_spring_23

7.2 添加依赖

<dependencies>
        <!--spring boot web-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!--nacos discovery-->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
        </dependency>
        <!--nacos config-->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
        </dependency>
        <!--sentinel-->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
        </dependency>

        <!--在资源服务器添加此依赖,只做授权,不做认证,添加完此依赖以后,
        在项目中我们要做哪些事情?对受限访问的资源可以先判断是否登录了,
        已经认证用户还要判断是否有权限?
        -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-oauth2</artifactId>
        </dependency>
    </dependencies>

7.3 配置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.4 创建启动类

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.5 创建Controller层

package com.jt.resource.controller;

import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class ResourceController {
    @PreAuthorize("hasAnyAuthority('sys:res:create')")//设置访问权限 权限中包含这个字符串才可访问这个资源
    @RequestMapping("/resource/select")//ResourceConfig中设置resource开头的请求需要认证
    public String doSelect(){
        return "Select Resource OK...";
    }
    /*ResourceConfig中设置res开头的请求直接放行*/
    @RequestMapping("/res/delete")
    public String doDelete(){
        return "Delete resource ok";
    }
}

gateway 微服务登录验证 微服务登陆_User_24

7.6 配置令牌解析器对象

package com.jt.resource.config;

import org.springframework.boot.autoconfigure.jersey.JerseyAutoConfiguration;
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 {
    /*配置令牌的存储策略,对于oauth2规范中提供了这样的几种策略
     * 1)JdbcTokenStore(这里是要将token存储到关系型数据库)
     * 2)RedisTokenStore(这是要将token存储到redis数据库-key/value)
     * 3)JwtTokenStore(这里是将产生的token信息存储客户端,并且token
     * 中可以以自包含的形式存储一些用户信息)
     * */
    @Bean
    public TokenStore tokenStore(){
        //采用JWT方式生成和存储令牌信息
        return new JwtTokenStore(jwtAccessTokenConverter());
    }

    @Bean
    public JwtAccessTokenConverter jwtAccessTokenConverter(){
        JwtAccessTokenConverter jwtAccessTokenConverter=new JwtAccessTokenConverter();
        jwtAccessTokenConverter.setSigningKey(SIGNING_KEY);
        return jwtAccessTokenConverter;
    }
    private static final String SIGNING_KEY="auth";
}

7.7 配置资源认证授权规则

package com.jt.resource.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;
import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter;
/*思考?对于一个系统而言,它资源的访问权限你是如何进行分类设计的
 * 1)不需要登录就可以访问(例如12306查票)
 * 2)登录以后才能访问(例如12306的购票)
 * 3)登录以后没有权限也不能访问(例如会员等级不够不让执行一些相关操作)
* */
@Configuration
@EnableResourceServer//启动资源服务默认配置
//访问资源服务器中的相关方法时启动权限调查 与@PreAuthorize配合使用
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class ResourceConfig extends ResourceServerConfigurerAdapter {//适配器模式
    @Override
    public void configure(HttpSecurity http) throws Exception {
//        super.configure(http);
        //关闭跨域攻. 击
        http.csrf().disable();
        //配置资源访问方式
        http.authorizeRequests().antMatchers("/resource/**")//只要访问这个资源就需要认证
                .authenticated()
                .anyRequest().permitAll();
    }
}

测试

首先需要认证登录,其次需要权限

gateway 微服务登录验证 微服务登陆_gateway 微服务登录验证_25


gateway 微服务登录验证 微服务登陆_spring_26


gateway 微服务登录验证 微服务登陆_spring_27


gateway 微服务登录验证 微服务登陆_微服务_28

  • 401 需要认证
  • 403 无权限
  • 404 路径不正确
  • 405 请求方式不匹配

8. 网关工程设计及实现

8.1 创建工程sso-gateway

gateway 微服务登录验证 微服务登陆_spring_29

8.2 添加依赖

">        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-gateway</artifactId>
        </dependency>
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
        </dependency>
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
        </dependency>
        <!--假如网关层面进行限流,添加如下依赖-->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
        </dependency>

        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-alibaba-sentinel-gateway</artifactId>
        </dependency>
    </dependencies>

8.3 配置yml文件

server:
  port: 9000
spring:
  application:
    name: sso-gateway
  cloud:
    nacos:
      discovery:
        server-addr: localhost:8848
      config:
        server-addr: localhost:8848
        file-extension: yml
    sentinel:
      transport:
        dashboard: localhost:8180
      eager: true
    gateway:
      routes:
        - id: router01
          uri: lb://sso-resource
          predicates:
            - Path=/sso/resource/**
          filters:
            - StripPrefix=1
        - id: router02
          uri: lb://sso-auth
          predicates:
            - Path=/sso/oauth/**
          filters:
            - StripPrefix=1
      globalcors: #跨域配置(写到配置文件的好处是可以将其配置写到配置中心)
        corsConfigurations:
          '[/**]':
            allowedOrigins: "*"
            allowedHeaders: "*"
            allowedMethods: "*"
            allowCredentials: true

8.4 创建启动类

package com.jt;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class ApiGatewayApplication {
    public static void main(String[] args) {
        SpringApplication.run(ApiGatewayApplication.class, args);
    }
}

9. 客户端UI工程设计及实现

9.1 创建sso-ui工程

gateway 微服务登录验证 微服务登陆_spring_30

9.2 创建UI工程登陆页面

<!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",//定义监控点,vue底层会基于此监控点在内存中构建dom树
        data:{ //此对象中定义页面上要操作的数据
            username:"",
            password:""
        },
        methods: {//此位置定义所有业务事件处理函数
            doLogin() {
                //1.定义url
                let url = "http://localhost:9000/sso/oauth/token"
                //2.定义参数
                let params = new URLSearchParams()
                params.append('username',this.username);
                params.append('password',this.password);
                params.append('client_id',"gateway-client");
                params.append('client_secret',"123456");
                params.append('grant_type',"password");
                //3.发送异步请求
                axios.post(url, params)
                    .then((response) => {//ok
                        alert("login ok")
                        let result=response.data;
                        console.log("result",result);
                        //将返回的访问令牌存储到浏览器本地对象中
                        localStorage.setItem("accessToken",result.access_token);
                        location.href="/resource.html";
                        //启动一个定时器,一个小时以后,向认证中心发送刷新令牌
                    })
                    .catch((e)=>{
                        console.log(e);
                    })
            }
        }
    });
</script>
</body>
</html>

9.3 创建资源展现页面

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<div>
    <h1>The Resource Page</h1>
    <button onclick="doSelect()">我的资源(例如我的订单)</button>
</div>
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
<script>
    function doSelect(){
        let url="http://localhost:9000/sso/resource/select";
        //获取登录后,存储到浏览器客户端的访问令牌
        let token=localStorage.getItem("accessToken");
        //发送请求时,携带访问令牌
        axios.get(url,{headers:{"Authorization":"Bearer "+token}})
            .then(function (response){
                alert("select ok")
                console.log(response.data);
            })
            .catch(function (e){//失败时执行catch代码块
                if(e.response.status==401){
                    alert("请先登录");
                    location.href="/login.html";
                }else if(e.response.status==403){
                    alert("您没有权限")
                }
                console.log("error",e);
            })
    }
</script>
</body>
</html>

实现效果

gateway 微服务登录验证 微服务登陆_微服务_31


gateway 微服务登录验证 微服务登陆_microservices_32

总结

  • 单点登陆系统解决方案?数据库、token
  • 为什么要设计单点登录系统??
  • 工程结构???

单体架构设计
httpsession(30mins,tomcat私有) cookie(存储sessionid) server