微服务版单点登录系统(Spring Security)

目录

简介

多点登录系统

单点登录系统​ 

快速入门案例

创建认证授权工程

添加项目依赖

 构建项目配置文件

添加项目启动类

 启动并访问项目

定义登录逻辑

业务描述

定义配置安全类

 定义用户信息处理对象

网关中登录路由配置 

基于Postman进行访问测试

自定义登录页面

颁发登录成功令牌

构建令牌配置对象

定义认证授权核心配置

配置网关认证的URL

Postman访问测试

 登录页面方案设计

资源服务器配置

添加依赖

令牌处理器配置

资源服务令牌解析配置

Controller 方法配置

启动服务访问测试

数据库访问

数据库初始化

创建工程

添加依赖

 逻辑实现

服务之间的调用

业务描述

 认证服务中Feign应用

单点登录-redis

技术摘要

Spring Security

SpringSecurity 执行流程

JWT数据规范

Oauth2规范


简介

多点登录系统

每个站点都实现了自己的专用登录模块。各站点的登录状态相互不认可,各站点需要逐一手工登录。应用起来相对繁琐(每次访问资源服务都需要重新登陆认证和授权)。与此同时,系统代码的重复也比较高。微服务版单点登录系统(Spring Security)_第1张图片

单点登录系统微服务版单点登录系统(Spring Security)_第2张图片 

快速入门案例

创建认证授权工程

添加项目依赖

 
        org.springframework.boot
        spring-boot-starter-web
   

   
        org.springframework.cloud
        spring-cloud-starter-oauth2
   

   
        com.alibaba.cloud
        spring-cloud-starter-alibaba-nacos-discovery
   

   
        com.alibaba.cloud
        spring-cloud-starter-alibaba-nacos-config
   

 

 构建项目配置文件

在工程中创建bootstrap.yml文件:

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

添加项目启动类

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

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

 启动并访问项目

项目启动时,系统会默认生成一个登陆密码,例如:微服务版单点登录系统(Spring Security)_第3张图片

打开浏览器输入http://localhost:8071呈现登陆页面,例如:微服务版单点登录系统(Spring Security)_第4张图片

其中,默认用户名为user,密码为系统启动时,在控制台呈现的密码。执行登陆测试,登陆成功进入如下界面(因为没有定义登陆页面,所以会出现404):微服务版单点登录系统(Spring Security)_第5张图片 

定义登录逻辑

业务描述

我们的单点登录系统最终会按照如下结构进行设计和实现,例如:

微服务版单点登录系统(Spring Security)_第6张图片

定义配置安全类

修改SecurityConfig配置类,添加登录成功或失败的处理逻辑,例如:

import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
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;

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    /**初始化密码加密对象*/
    @Bean
    public BCryptPasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }
    /**在这个方法中定义登录规则
     * 1)对所有请求放行(当前工程只做认证)
     * 2)登录成功信息的返回
     * 3)登录失败信息的返回
     * */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        //关闭跨域工具
        http.csrf().disable();
        //放行所有请求
        http.authorizeRequests().anyRequest().permitAll();
        //登录成功与失败的处理
        http.formLogin()
                .successHandler(successHandler())
                .failureHandler(failureHandler());
    }

    @Bean
    public AuthenticationSuccessHandler successHandler(){
//        return new AuthenticationSuccessHandler() {
//            @Override
//            public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
//
//            }
//        }
        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, e)-> {
            //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();
    }
}
 

 定义用户信息处理对象

在spring security应用中底层会借助UserDetailService对象获取数据库信息,并进行封装,最后返回给认证管理器,完成认证操作,例如:

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
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;

/**
 * 登录时用户信息的获取和封装会在此对象进行实现,
 * 在页面上点击登录按钮时,会调用这个对象的loadUserByUsername方法,
 * 页面上输入的用户名会传给这个方法的参数
 */
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
    @Autowired
    private BCryptPasswordEncoder passwordEncoder;
    //UserDetails用户封装用户信息(认证和权限信息)

    @Override
    public UserDetails loadUserByUsername(String username)
            throws UsernameNotFoundException {
        //1.基于用户名查询用户信息(用户名,用户状态,密码,....)
        //Userinfo userinfo=userMapper.selectUserByUsername(username);
        String encodedPassword=passwordEncoder.encode("123456");
        //2.查询用户权限信息(后面会访问数据库)
        //这里先给几个假数据
        List authorities =
        AuthorityUtils.createAuthorityList(//这里的权限信息先这么写,后面讲
                "sys:res:create", "sys:res:retrieve");
        //3.对用户信息进行封装
        return new User(username,encodedPassword,authorities);
    }
}

网关中登录路由配置 

在网关配置文件中添加登录路由配置,例如

 - id: router02
   uri: lb://sca-auth  #lb表示负载均衡,底层默认使用ribbon实现
   predicates: #定义请求规则(请求需要按照此规则设计)
      - Path=/auth/login/** #请求路径设计
   filters:
      - StripPrefix=1 #转发之前去掉path中第一层路径
 

基于Postman进行访问测试

启动gateway和auth,然后基于postman访问网关,执行登录测试,例如:微服务版单点登录系统(Spring Security)_第7张图片 

自定义登录页面

在ui工程的static目录中定义登陆页面,例如: 




   
   
   
   
   
    login



   

Please Login


   

       

           
           
       

       

           
           
       

       
   







启动ui服务后,进入登陆页面,输入用户名jack,密码123456进行登陆测试。

颁发登录成功令牌

构建令牌配置对象

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

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 {
    //定义签名key,在执行令牌签名需要这个key,可以自己指定.
     private String SIGNING_KEY = "auth";

    //定义令牌生成策略.
    @Bean
    public TokenStore tokenStore() {
        return new JwtTokenStore(jwtAccessTokenConverter());
    }
    //定义Jwt转换器,负责生成jwt令牌,解析令牌内容
    @Bean
    public JwtAccessTokenConverter jwtAccessTokenConverter(){
        JwtAccessTokenConverter converter=new JwtAccessTokenConverter();
        //设置加密/解密口令
        converter.setSigningKey(SIGNING_KEY);
        return converter;
    }  

定义认证授权核心配置

1.在SecurityConfig中添加如下方法(创建认证管理器对象,后面授权服务器会用到):

 @Bean
    public AuthenticationManager authenticationManagerBean()
            throws Exception {
        return super.authenticationManagerBean();
    }

2.所有零件准备好了开始拼装最后的主体部分,这个主体部分就是授权服务器的核心配置

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;

/**
 * 完成所有配置的组装,在这个配置类中完成认证授权,JWT令牌签发等配置操作
 * 1)SpringSecurity (安全认证和授权)
 * 2)TokenConfig
 * 3)Oauth2(暂时不说)
 */

@AllArgsConstructor
@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(){
        //这个方法的目标就是获得一个令牌生成器
        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 {
        //认证通过后,允许客户端进行哪些操作
        security
        //公开oauth/token_key端点
        .tokenKeyAccess("permitAll()")
        //公开oauth/check_token端点
        .checkTokenAccess("permitAll()")
        //允许提交请求进行认证(申请令牌)
        .allowFormAuthenticationForClients();
    }
}

配置网关认证的URL

  - id: router02
      uri: lb://sca-auth
      predicates:
        #- Path=/auth/login/**  #没要令牌之前,以前是这样配置
        - Path=/auth/oauth/**   #微服务架构下,需要令牌,现在要这样配置
      filters:
        - StripPrefix=1
 

Postman访问测试

1.启动服务

依次启动auth服务,gateway服务

2.检测auth服务控制台的Endpoints信息

微服务版单点登录系统(Spring Security)_第8张图片

3.打开postman进行登录访问测试

微服务版单点登录系统(Spring Security)_第9张图片

登陆成功会在控制台显示令牌信息,例如:

{
    "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2Mjk5OTg0NjAsInVzZXJfbmFtZSI6ImphY2siLCJhdXRob3JpdGllcyI6WyJzeXM6cmVzOmNyZWF0ZSIsInN5czpyZXM6cmV0cmlldmUiXSwianRpIjoiYWQ3ZDk1ODYtMjUwYS00M2M4LWI0ODYtNjIyYjJmY2UzMDNiIiwiY2xpZW50X2lkIjoiZ2F0ZXdheS1jbGllbnQiLCJzY29wZSI6WyJhbGwiXX0.-Zcmxwh0pz3GTKdktpr4FknFB1v23w-E501y7TZmLg4",
    "token_type": "bearer",
    "refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX25hbWUiOiJqYWNrIiwic2NvcGUiOlsiYWxsIl0sImF0aSI6ImFkN2Q5NTg2LTI1MGEtNDNjOC1iNDg2LTYyMmIyZmNlMzAzYiIsImV4cCI6MTYzMDI1NDA2MCwiYXV0aG9yaXRpZXMiOlsic3lzOnJlczpjcmVhdGUiLCJzeXM6cmVzOnJldHJpZXZlIl0sImp0aSI6IjIyOTdjMTg2LWM4MDktNDZiZi1iNmMxLWFiYWExY2ExZjQ1ZiIsImNsaWVudF9pZCI6ImdhdGV3YXktY2xpZW50In0.1Bf5IazROtFFJu31Qv3rWAVEtFC1NHWU1z_DsgcnSX0",
    "expires_in": 3599,
    "scope": "all",
    "jti": "ad7d9586-250a-43c8-b486-622b2fce303b"
}
 

 登录页面方案设计

登陆成功以后,将token存储到localStorage中,修改登录页面的doLogin方法,例如

  doLogin() {
    //1.定义url
     let url = "http://localhost:9000/auth/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) => {
          alert("login ok");
           let result=response.data;
           localStorage.setItem("accessToken",result.access_token);
            location.href="/fileupload.html";
          }).catch((error)=>{
                    console.log(error);
         })
       }
 

资源服务器配置

添加依赖

打开资源服务的pom.xml文件,添加oauth2依赖。


    org.springframework.cloud
    spring-cloud-starter-oauth2

 

令牌处理器配置

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;

/**
 * 创建JWT令牌配置类,基于这个类实现令牌对象的创建和解析.
 * JWT令牌的构成有三部分构成:
 * 1)HEADER (头部信息:令牌类型,签名算法)
 * 2)PAYLOAD (数据信息-用户信息,权限信息,令牌失效时间,...)
 * 3)SIGNATURE (签名信息-对header和payload部分进行加密签名)
 */
@Configuration
public class TokenConfig {
    //定义令牌签发口令(暗号),这个口令自己定义即可
    //在对header和PAYLOAD部分进行签名时,需要的一个口令
    private String SIGNING_KEY= "auth";
    //初始化令牌生成策略(默认生成策略 UUID)
    //这里我们采用JWT方式生成令牌
    @Bean
    public TokenStore tokenStore(){
        return new JwtTokenStore(jwtAccessTokenConverter());
    }
    //构建JWT令牌转换器对象,基于此对象创建令牌,解析令牌
    @Bean
    public JwtAccessTokenConverter jwtAccessTokenConverter(){
        JwtAccessTokenConverter converter=new JwtAccessTokenConverter();
        converter.setSigningKey(SIGNING_KEY);
        return converter;
    }
}
 

资源服务令牌解析配置

import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.beans.factory.annotation.Autowired;
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;
import org.springframework.security.oauth2.config.annotation.web.configurers.ResourceServerSecurityConfigurer;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.web.access.AccessDeniedHandler;

import javax.servlet.http.HttpServletResponse;
import java.io.PrintWriter;
import java.util.HashMap;
import java.util.Map;

@Configuration
@EnableResourceServer
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {

    @Autowired
    private TokenStore tokenStore;
    /**
     * token服务配置
     */
    @Override
    public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
        resources.tokenStore(tokenStore);
    }
    /**
     * 路由安全认证配置
     */
    @Override
    public void configure(HttpSecurity http) throws Exception {
        http.csrf().disable();
        http.exceptionHandling()
                .accessDeniedHandler(accessDeniedHandler());
        http.authorizeRequests().anyRequest().permitAll();

    }
    //没有权限时执行此处理器方法
    public AccessDeniedHandler accessDeniedHandler() {
        return (request, response, e) -> {
            Map map = new HashMap<>();
            map.put("state", HttpServletResponse.SC_FORBIDDEN);//SC_FORBIDDEN的值是403
            map.put("message", "没有访问权限,请联系管理员");
            //1设置响应数据的编码
            response.setCharacterEncoding("utf-8");
            //2告诉浏览器响应数据的内容类型以及编码
            response.setContentType("application/json;charset=utf-8");
            //3获取输出流对象
            PrintWriter out=response.getWriter();
            //4 输出数据
            String result=
                    new ObjectMapper().writeValueAsString(map);
            out.println(result);
            out.flush();
        };
    }

}
 

Controller 方法配置

在controller的上传方法上添加 @PreAuthorize(“hasAuthority(‘sys:res:create’)”)注解,用于告诉底层框架方法此方法需要具备的权限,例如

  @PreAuthorize("hasAuthority('sys:res:create')")
  @PostMapping("/upload/")
   public String uploadFile(MultipartFile uploadFile) throws IOException {
       ...
   }
 

启动服务访问测试

1.启动服务:auth\gatway\资源服务

2.执行登陆获取access_token令牌

3.携带令牌访问资源(url中的前缀"sca"是在资源服务器中自己指定的,你的网关怎么配置的,你就怎么写)

设置请求头(header),要携带令牌并指定请求的内容类型,例如微服务版单点登录系统(Spring Security)_第10张图片

设置请求体(body),设置form-data,key要求为file类型,参数名与你服务端controller文件上传方法的参数名相同,值为你选择的文件,例如

微服务版单点登录系统(Spring Security)_第11张图片

上传成功会显示你访问文件需要的路径,假如没有权限会提示你没有访问权限。

数据库访问

数据库初始化

将sql文件在mysql中执行:

1.登录mysql

mysql -uroot -proot

2.通过source指令执行sql文件

source d:/jt-sso.sql

创建工程

微服务版单点登录系统(Spring Security)_第12张图片

添加依赖

        
        
        
            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
        

 逻辑实现

pojo对象

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对象

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 {
    /**
     * 基于用户名获取用户信息
     * @param username
     * @return
     */
    @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 selectUserPermissions(Long userId);

}

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

服务之间的调用

业务描述

系统服务system工程用于提供,其它服务需要的基础数据,例如用户信息,日志信息的记录等,其关键表设计例如:

微服务版单点登录系统(Spring Security)_第13张图片

 认证服务中Feign应用

1.业务描述

在认证auth工程中,我们通过调用system服务获取登录用户信息,用户权限信息.

2.添加依赖(auth)

  
        org.springframework.cloud
        spring-cloud-starter-openfeign
  

3.定义pojo对象

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

4.feign接口定义

package com.jt.auth.feign;
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(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")Long userId);
}

说明,feign接口定义后,需要在auth启动类上添加@EnableFeignClients注解.

5.调用Feign接口

在auth工程中的UserDetailServiceImpl中添加对feign接口的调用,例如:

package com.jt.auth.service;

import com.jt.auth.feign.RemoteUserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
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;

/**
 * 登录时用户信息的获取和封装会在此对象进行实现,
 * 在页面上点击登录按钮时,会调用这个对象的loadUserByUsername方法,
 * 页面上输入的用户名会传给这个方法的参数
 */
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
    @Autowired
    private BCryptPasswordEncoder passwordEncoder;

    @Autowired
    private RemoteUserService remoteUserService;
    //UserDetails用户封装用户信息(认证和权限信息)
    @Override
    public UserDetails loadUserByUsername(String username)
            throws UsernameNotFoundException {
        //1.基于用户名查询用户信息(用户名,用户状态,密码,....)
        com.jt.auth.pojo.User user=
                remoteUserService.selectUserByUsername(username);
        //2.查询用户权限信息(后面会访问数据库)
        List permissions=
        remoteUserService.selectUserPermissions(user.getId());
        System.out.println("permissions="+permissions);
        List authorities =
        AuthorityUtils.createAuthorityList(//这里的权限信息先这么写,后面讲
                permissions.toArray(new String[]{}));
        //3.对用户信息进行封装
        return new User(username,user.getPassword(),authorities);
    }
}

6.启动所有工程进行测试.

单点登录-redis

微服务版单点登录系统(Spring Security)_第14张图片

 

技术摘要

Spring Security

Spring Security 是一个企业级安全框架,由spring官方推出,它对软件系统中的认证,授权,加密等功能进行封装,并在springboot技术推出以后,配置方面做了很大的简化.现在市场上分布式架构中的安全控制,正在逐步的转向Spring Security。Spring Security 在企业中实现认证和授权业务时,底层构建了大量的过滤器,如图所示:
微服务版单点登录系统(Spring Security)_第15张图片

 其中:
图中绿色部分为认证过滤器,黄色部分为授权过滤器。Spring Security就是通过这些过滤器然后调用相关对象一起完成认证和授权操作.

SpringSecurity 执行流程

微服务版单点登录系统(Spring Security)_第16张图片

JWT数据规范

JWT(JSON WEB Token)是一个标准,采用数据自包含方式进行json格式数据设计,实现各方安全的信息传输,其官方网址为:https://jwt.io/。官方JWT规范定义,它构成有三部分,分别为Header(头部),Payload(负载),Signature(签名)

Header(头部)

Header 部分是一个 JSON 对象,描述 JWT 的元数据,通常是下面的样子。

{
  "alg": "HS256",
  "typ": "JWT"
}
 

上面代码中,alg属性表示签名的算法(algorithm),默认是 HMAC SHA256(简写HS256);typ属性表示这个令牌(token)的类型(type),JWT 令牌统一写为JWT。最后,将这个 JSON 对象使用 Base64URL 算法(详见后文)转成字符串。

Payload(负载)

Payload 部分也是一个 JSON 对象,用来存放实际需要传递的数据。JWT规范中规定了7个官方字段,供选用。

  • iss (issuer):签发人
  • exp (expiration time):过期时间
  • sub (subject):主题
  • aud (audience):受众
  • nbf (Not Before):生效时间
  • iat (Issued At):签发时间
  • jti (JWT ID):编号

除了官方字段,你还可以在这个部分定义私有字段,下面就是一个例子。
{
  "sub": "1234567890",
  "name": "John Doe",
  "admin": true
}
 

注意,JWT 默认是不加密的,任何人都可以读到,所以不要把秘密信息放在这个部分。

这个 JSON 对象也要使用 Base64URL 算法转成字符串。

Signature(签名)

Signature 部分是对前两部分的签名,其目的是防止数据被篡改。

首先,需要指定一个密钥(secret)。这个密钥只有服务器才知道,不能泄露给用户。然后,使用 Header 里面指定的签名算法(默认是 HMAC SHA256),按照下面的公式产生签名。

HMACSHA256(
  base64UrlEncode(header) + "." +
  base64UrlEncode(payload),
  secret)
 

算出签名以后,把 Header、Payload、Signature 三个部分拼成一个字符串,每个部分之间用"点"(.)分隔,就可以返回给用户。

Oauth2规范


oauth2定义了一种认证授权协议,一种规范,此规范中定义了四种类型的角色:
1)资源有者(User)
2)认证授权服务器(jt-auth)
3)资源服务器(jt-resource)
4)客户端应用(jt-ui)
同时,在这种协议中规定了认证授权时的几种模式:
1)密码模式 (基于用户名和密码进行认证)
2)授权码模式(就是我们说的三方认证:QQ,微信,微博,。。。。)
3)…
 

你可能感兴趣的:(#,spring微服务,spring)