孤尽T31训练营11权限管理笔记

这是我参与11月更文挑战的第7天,活动详情查看:2021最后一次更文挑战

这次课程主要是实践,这里记录一下搭建auth-center的过程

准备:生成密钥证书

第1步:生成密钥证书

下面的命令生成密钥证书,采用RSA算法,每个证书包含公钥和私钥

-- 创建一个文件夹,在该文件夹下执行如下命令
keytool -genkeypair -alias kaikeba -keyalg RSA -keypass kaikeba -keystore kaikeba.jks -storepass kaikeba

-alias:密钥的别名 
-keyalg:使用的hash算法 
-keypass:密钥的访问密码 
-keystore:密钥库文件名 
-storepass:密钥库的访问密码
复制代码

image-20211122222036588.png

生成证书文件kaikeba.jks,搭建授权服务器时用

第2步:证书管理

-- 查询证书信息
keytool -list -keystore kaikeba.jks

-- 删除别名
keytool -delete -alias kaikeba -keystore kaikeba.jsk
复制代码

image-20211122222243394.png

第3步:导出公钥

keytool -list -rfc --keystore kaikeba.jks | openssl x509 -inform pem -pubkey
复制代码

将公钥保存到public.key文件,搭建资源服务器时用

搭建授权中心

第1步:创建授权中心模块

t31-auth-center

第2步:添加依赖

pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>t31-parent</artifactId>
        <groupId>com.kaikeba</groupId>
        <version>1.0.0</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>t31-auth-center</artifactId>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>

        <!--SpringCloud ailibaba nacos -->
        <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>
        <!--SpringCloud ailibaba sentinel -->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
        </dependency>

        <!--oauth2-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-oauth2</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jdbc</artifactId>
        </dependency>

        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.1.46</version>
            <scope>runtime</scope>
        </dependency>

        <dependency>
            <groupId>com.kaikeba</groupId>
            <artifactId>t31-admin-instance</artifactId>
            <version>${project.version}</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-resources-plugin</artifactId>
                <configuration>
                    <nonFilteredFileExtensions>
                        <nonFilteredFileExtension>jks</nonFilteredFileExtension>
                    </nonFilteredFileExtensions>
                </configuration>
            </plugin>
        </plugins>
    </build>

</project>
复制代码

第3步:配置

application.yml

spring:
  application:
    name: auth-center
  profiles:
    active: dev
复制代码

bootstrap.yml

spring:
  cloud:
    nacos:
      config:
        server-addr: localhost:8848
        file-extension: yaml
        extension-configs[0]:
          data-id: common.yaml
          refresh: true
        extension-configs[1]:
          data-id: db.yaml
          refresh: true
  # 多个接口上的@FeignClient(“相同服务名”)会报错,overriding is disabled。
  # 设置 为true ,即 允许 同名
  main:
    allow-bean-definition-overriding: true
复制代码

生成的密钥证书放到resources下

第4步:启动器

AuthApplication.java

package com.kaikeba.t31.auth;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.circuitbreaker.EnableCircuitBreaker;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.openfeign.EnableFeignClients;

/**
 * 授权中心
 *
 * @author yangdc
 * @date 2021/11/22
 */
@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients
@EnableCircuitBreaker
public class AuthApplication {

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

复制代码

第5步:OAuth2配置

package com.kaikeba.t31.auth.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;
import org.springframework.security.authentication.AuthenticationManager;
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.ClientDetailsService;
import org.springframework.security.oauth2.provider.client.JdbcClientDetailsService;
import org.springframework.security.oauth2.provider.token.DefaultTokenServices;
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;
import org.springframework.security.oauth2.provider.token.store.KeyStoreKeyFactory;

import javax.sql.DataSource;
import java.util.concurrent.TimeUnit;


/**
 * OAuth2配置
 *
 * @author yangdc
 * @date 2021/11/22
 */
@Configuration
@EnableAuthorizationServer
public class OAuth2Config extends AuthorizationServerConfigurerAdapter {

    @Autowired
    @Qualifier("authenticationManagerBean")
    AuthenticationManager authenticationManager;

    @Autowired
    private DataSource dataSource;

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

    @Bean
    public JwtAccessTokenConverter jwtAccessTokenConverter() {
        //证书路径和密钥库密码
        KeyStoreKeyFactory keyStoreKeyFactory = new KeyStoreKeyFactory(new ClassPathResource("kaikeba.jks"), "kaikeba".toCharArray());
        JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
        //密钥别名
        converter.setKeyPair(keyStoreKeyFactory.getKeyPair("kaikeba"));
        return converter;
    }

    @Bean
    public ClientDetailsService clientDetailsService() {
        return new JdbcClientDetailsService(dataSource);
    }

    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        //配置通过表oauth_client_details,读取客户端数据
        clients.withClientDetails(clientDetailsService());
    }

    /**
     * 配置token service和令牌存储方式(tokenStore
     * @param endpoints
     * @throws Exception
     */
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        //tokenStore
        endpoints.tokenStore(tokenStore()).tokenEnhancer(jwtAccessTokenConverter()).authenticationManager(authenticationManager);

        //tokenService
        DefaultTokenServices tokenServices = new DefaultTokenServices();
        tokenServices.setTokenStore(endpoints.getTokenStore());
        tokenServices.setSupportRefreshToken(false);
        tokenServices.setClientDetailsService(endpoints.getClientDetailsService());
        tokenServices.setTokenEnhancer(endpoints.getTokenEnhancer());
        // 30天
        tokenServices.setAccessTokenValiditySeconds((int) TimeUnit.DAYS.toSeconds(30));
        endpoints.tokenServices(tokenServices);
    }

    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
        // 允许表单认证
        security.allowFormAuthenticationForClients()
                //放行oauth/token_key(获得公钥)
                .tokenKeyAccess("permitAll()")
                //放行 oauth/check_token(验证令牌)
//                .checkTokenAccess("isAuthenticated()");
                .checkTokenAccess("permitAll()");
    }
}

复制代码

第6步:Spring Security配置

package com.kaikeba.t31.auth.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

/**
 * Security配置
 *
 * @author yangdc
 * @date 2021/11/22
 */
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    /**
     * 注入自定义UserDetailService,读取rbac数据
     */
    @Autowired
    private UserDetailsService userDetailsService;

    @Bean
    PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

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

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // 开放/oauth/开头的所有请求
        http.requestMatchers().anyRequest()
                .and().authorizeRequests().antMatchers("/oauth/**").permitAll();
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        // 注入自定义的UserDetailsService,采用BCrypt加密
        auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
    }
}

复制代码

第7步:UserDetailService

UserDetailService作用是从数据库中读取rbac数据,用户、密码、角色数据返回UserDetails,交给Spring Security框架匹配验证,颁发令牌等操作

package com.kaikeba.t31.auth.service.impl;

import com.kaikeba.t31.admin.po.Role;
import com.kaikeba.t31.auth.client.UserClient;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
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.password.PasswordEncoder;
import org.springframework.stereotype.Service;

import java.util.ArrayList;
import java.util.List;

/**
 * 用户详情服务实现
 * 作用:读取数据库用户角色数据
 *
 * @author yangdc
 * @date 2021/11/22
 */
@Slf4j
@Service
public class UserDetailServiceImpl implements UserDetailsService {

    @Autowired
    UserClient userClient;

    @Autowired
    PasswordEncoder passwordEncoder;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        // 使用OpenFeign调用admin-service,得到用户角色数据
        com.kaikeba.t31.admin.po.User user = userClient.getByUsername(username);
        List<GrantedAuthority> grantedAuthorities = new ArrayList<>();
        if (user != null) {
            log.debug("current user = " + user);
            // 获取用户的授权
            List<Role> roles = userClient.selectRoleByUserId(user.getId());
            // 声明授权文件
            for (Role role : roles) {
                if (role != null && role.getName() != null) {
                    // 封闭用户角色信息,必须ROLE_开头
                    // spring Security中权限名称必须满足ROLE_XXX
                    GrantedAuthority grantedAuthority = new SimpleGrantedAuthority("ROLE_" + role.getName());
                    grantedAuthorities.add(grantedAuthority);
                }
            }
        }
        log.debug("granted authorities = " + grantedAuthorities);
        // 返回Spring Security框架的User对象
        return new User(user.getUserName(), user.getPassword(), grantedAuthorities);
    }
}

复制代码

读取数据的UserFeign如下

package com.kaikeba.t31.auth.client;

import com.kaikeba.t31.admin.po.Role;
import com.kaikeba.t31.admin.po.User;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.RequestMapping;

import java.util.List;

/**
 * 用户客户端
 *
 * @author yangdc
 * @date 2021/11/22
 */
@FeignClient(name = "admin-service", fallback = UserClient.UserClientFallback.class)
public interface UserClient extends UserApi {

    @Slf4j
    @Component
    //这个可以避免容器中requestMapping重复
    @RequestMapping("/fallback")
    class UserClientFallback implements UserClient {

        @Override
        public User getByUsername(String username) {
            log.info("异常发生,进入fallback方法");
            return null;
        }

        @Override
        public List<Role> selectRoleByUserId(Long id) {
            log.info("异常发生,进入fallback方法");
            return null;
        }

    }
}

复制代码

搭建资源中心

t31-admin-service服务

第0步:配置

先前public.key复制到resources下

第1步:Jwt配置

package com.kaikeba.t31.admin.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
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;
import org.springframework.util.FileCopyUtils;

import java.io.IOException;

/**
 * Jwt配置
 *
 * @author yangdc
 * @date 2021/11/22
 */
@Configuration
public class JwtConfig {

    public static final String public_cert = "public.key";

    @Autowired
    private JwtAccessTokenConverter jwtAccessTokenConverter;

    @Bean
    @Qualifier("tokenStore")
    public TokenStore tokenStore() {
        return new JwtTokenStore(jwtAccessTokenConverter);
    }

    @Bean
    protected JwtAccessTokenConverter jwtAccessTokenConverter() {
        JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
        Resource resource =  new ClassPathResource(public_cert);

        String publicKey;
        try {
            publicKey = new String(FileCopyUtils.copyToByteArray(resource.getInputStream()));
        }catch (IOException e) {
            throw new RuntimeException(e);
        }

        // 设置校验公钥
        converter.setVerifierKey(publicKey);

        // 设置证书签名密码,否则报错
        converter.setSigningKey("kaikeba");

        return converter;
    }
}

复制代码

第2步:资源服务器配置

package com.kaikeba.t31.admin.config;

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;

/**
 * 资源服务器配置
 *
 * @author yangdc
 * @date 2021/11/22
 */
@Configuration
@EnableResourceServer
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {

    @Autowired
    private TokenStore tokenStore;

    @Override
    public void configure(HttpSecurity http) throws Exception {
        http.csrf().disable()
                .authorizeRequests()
                // .antMatchers("/**").permitAll();
                .antMatchers("/user/**").permitAll()
                // 用于测试
                .antMatchers("/book/**").hasRole("ADMIN")
                .antMatchers("/**").authenticated();
    }

    @Override
    public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
        resources.tokenStore(tokenStore);
    }
}

复制代码

第3步:security.yml

所有资源中心都要配置oauth/token_key,用来验证公钥,启动时验证

resources/security.yml

security:
  oauth2:
    resource:
      jwt:
	      #如果使用JWT,可以获取公钥用于 token 的验签
        key-uri: http://localhost:9098/oauth/token_key
复制代码

第4步:权限控制说明

Spring Security中定义了四个支持权限控制的表达式注解,分别是

  • @PreAuthorize
  • @PostAuthorize
  • @PreFilter
  • @PostFilter

其中前两者可以用来在方法调用前或者调用后进行权限检查,后两者可以用来对集合类型的参数或者返回值进行过滤.在需要控制权限的方法上, 我们可以添加@PreAuthorize注解,用于方法执行前进行权限检查,校验用户当前角色是否能访问该方法.

1 开启EnableGlobalMethodSecurity

使用上述注解控制权限需要设置EnableGlobalMethodSecurity -----见第2步资源服务器配置

2 如何使用注解

package com.kaikeba.t31.admin.controller;

import lombok.extern.slf4j.Slf4j;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.provider.OAuth2Authentication;
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.security.Principal;

/**
 * @author yangdc
 * @date 2021/11/22
 */
@Slf4j
@RestController
public class TestController {

    @GetMapping("/product/{id}")
    public String getProduct(@PathVariable String id) {
        return "product id : " + id;
    }

    @GetMapping("/order/{id}")
    public String getOrder(@PathVariable String id) {
        return "order id : " + id;
    }

    @GetMapping("/book/{id}")
    public String getBook(@PathVariable String id) {
        return "book id : " + id;
    }

    @GetMapping("/anno/{id}")
    @PreAuthorize("hasRole('ROLE_ADMIN')")
    public String getAnno(@PathVariable String id) {
        return "admin id :" + id;
    }

    @RequestMapping("/hello")
    @PreAuthorize("hasAnyAuthority('ROLE_ADMIN')")
    public String hello() {
        return "hello you ...";
    }


    @GetMapping("/getPrinciple")
    public OAuth2Authentication getPrinciple(OAuth2Authentication oAuth2Authentication, Principal principal, Authentication authentication) {
        log.info(oAuth2Authentication.getUserAuthentication().getAuthorities().toString());
        log.info(oAuth2Authentication.toString());
        log.info("principal.toString() " + principal.toString());
        log.info("principal.getName() " + principal.getName());
        log.info("authentication: " + authentication.getAuthorities().toString());

        return oAuth2Authentication;
    }

}

复制代码

3 控制权限的方式---全局代码控制

image-20211122230658799.png

4 控制权限的方式---注解控制

image-20211122230740959.png

猜你喜欢

转载自juejin.im/post/7033497025584824334