Spring Cloud:认证 授权 OAuth2、JWT

北战南征 提交于 2020-08-12 04:54:41

OAuth2

OAuth2是当前授权的行业标准,其重点在于为Web应用程序、桌面应用程序、移动设备以及室内设备的授权流程提供简单的客户端开发方式。它为第三方应用提供对HTTP服务的有限访问,既可以是资源拥有者通过授权允许第三方应用获取HTTP服务,也可以是第三方以自己的名义获取访问权限。

角色

OAuth2中主要分为了4种角色:

  • Resource Owner(资源所有者),是能够对受保护的资源授予访问权限的实体,可以是一个用户,这时会称为终端用户(end-user)。
  • Resource Server(资源服务器),持有受保护的资源,允许持有访问令牌(Access Token)的请求访问受保护资源。
  • Client(客户端),持有资源所有者的授权,代表资源所有者对受保护资源进行访问。
  • Authorization Server(授权服务器),对资源所有者的授权进行认证,成功后向客户端发送访问令牌。

很多时候,资源服务器和授权服务器是合二为一的,在授权交互的时候作为授权服务器,在请求资源交互时作为资源服务器

Resource Server的配置

Resource Server(可以是授权服务器,也可以是其他的资源服务)提供了受OAuth2保护的资源,这些资源为API接口、Html页面、Js文件等.Spring OAuth2提供了实现此保护功能的Spring Security认证过滤器。在加了@Configuration注解的配置类上加@EnableResourceServer注解,开启Resource Server的功能

JWT

JSON Web Token(JWT)是一种开放的标准(RFC 7519),JWT定义了一种紧凑且自包含的标准,该标准旨在将各个主体的信息包装为JSON对象。主体信息是通过数字签名进行加密和验证的。常使用HMAC算法或RSA(公钥/私钥的非对称性加密)算法对JWT进行签名,安全性很高。

  • 紧凑性(compact):由于是加密后的字符串,JWT数据体积非常小,可通过POST请求参数或HTTP请求头发送。另外,数据体积小意味着传输速度很快。
  • 自包含(self-contained):JWT包含了主体的所有信息,所以避免了每个请求都需要向Uaa服务验证身份,降低了服务器的负载。

JWT由3个部分组成,分别以“.”分隔,组成部分如下。

  • Header(头):Header通常由两部分组成:令牌的类型(即JWT)和使用的算法类型,如HMAC、SHA256和RSA
  • Payload(有效载荷):了用户的一些信息和Claim(声明、权利)。有3种类型的Claim:保留、公开和私人
  • Signature(签名):需要将Base64编码后的Header、Payload和密钥进行签名

uaa配置

1. 依赖:

    implementation 'org.springframework.boot:spring-boot-starter-actuator'
    implementation 'org.springframework.boot:spring-boot-starter-web'
    implementation 'org.mybatis.spring.boot:mybatis-spring-boot-starter:2.1.2'
    implementation 'org.springframework.cloud:spring-cloud-starter-oauth2:2.2.2.RELEASE'
    implementation 'org.springframework.cloud:spring-cloud-starter-consul-discovery:2.2.2.RELEASE'
    runtimeOnly 'mysql:mysql-connector-java'

2. 配置文件:

bootstrap.yml:

spring:
  application:
    name: consul-auth
  cloud:
    consul:
      host: localhost
      port: 8500
      discovery:
        prefer-ip-address: true
        service-name: ${spring.application.name}


management:
  endpoints:
    web:
      exposure:
        include: "*"
  endpoint:
    health:
      show-details: always

application.yml:

server:
  port: 8731

spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/ffzs?useUnicode=true&zeroDateTimeBehavior=convertToNull&autoReconnect=true&characterEncoding=utf-8
    username: root
    password: 123zxc

  main:
    allow-bean-definition-overriding: true

3. 安装mysql数据库

需要通过数据库进行对人员登录人员数据进行存储这里使用了Mysql数据库

使用docker安装:

    mysql:
      image: mysql
      networks:
        - spring
      restart: always
      ports:
        - 33060:33060
        - 3306:3306
      volumes:
        - ./mysql/db:/var/lib/mysql
        - ./mysql/conf.d:/etc/mysql/conf.d
      environment:
        - MYSQL_ROOT_PASSWORD=123zxc    
      command: --default-authentication-plugin=mysql_native_password

创建一个user的表用来存储人员数据:

CREATE TABLE `myUser` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `password` varchar(255) DEFAULT NULL,
  `username` varchar(255) NOT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `name` (`username`)
) ENGINE=InnoDB AUTO_INCREMENT=0 DEFAULT CHARSET=utf8;

BEGIN;
INSERT INTO `myUser` VALUES ('1', '$2a$10$uuFQKbr2q/8aqYlPEBlRw.Z9UrtEPrydIh7IUXaEGVWBowY8mZrUq', 'ffzs'),('2', '$2a$10$QgQ9OtiCMnGzYGPabDzOkeBda0Sb8wqzwnTSErJWPx4GfeNOUvh7q', 'sleepycat');
COMMIT;

添加两个用户

4. AuthorizationServer配置

/**
 * @author ffzs
 * @describe
 * @date 2020/6/7
 */

@Configuration
@EnableAuthorizationServer
public class OAuthConfiguration extends AuthorizationServerConfigurerAdapter {

    @Autowired
    private AuthenticationManager authenticationManager;

    @Autowired
    private UserDetailsServiceImpl userServiceDetail;

    /**
     * 配置客户端信息
     * @param clients
     * @throws Exception
     */
    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {

        clients.inMemory()
                .withClient("consul_server")  // 用户端id 需要在Authorization Server中是唯一的。
                .secret("123456") // 连接密码
                .scopes("server")  // 配置的客户端域为 service
//                .autoApprove(true)   // client_secret
//                .authorities("ROLE_ADMIN", "ROLE_USER")  // 权限信息
                .authorizedGrantTypes("implicit", "refresh_token", "password", "authorization_code", "client_credentials")  // 类验证类型
                .accessTokenValiditySeconds(60*60);  //失效时间
    }

    /**
     * 配置授权Token的节点和Token服务
     * @param endpoints
     * @throws Exception
     */
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        endpoints
                .authenticationManager(authenticationManager) //只有配置了该选项,密码认证才会开启。在大多数情况下都是密码验证,所以一般都会配置这个选项
                /**
                 * 需要设置Token的管理策略,目前支持以下3种:
                 * InMemoryTokenStore:Token存储在内存中。
                 * JdbcTokenStore:Token存储在数据库中。需要引入spring-jdbc的依赖包,并配置数据源,以及初始化Spring OAuth2的数据库脚本
                 * JwtTokenStore:采用JWT形式,这种形式没有做任何的存储,因为JWT本身包含了用户验证的所有信息,不需要存储。采用这种形式,需要引入spring-jwt的依赖
                 */
                .tokenStore(jwtTokenStore())
                .tokenEnhancer(jwtTokenEnhancer())
//                .userDetailsService(userServiceDetail)  // 配置获取用户认证信息的接口
                .allowedTokenEndpointRequestMethods(HttpMethod.GET, HttpMethod.POST);
    }

    @Bean
    public TokenStore jwtTokenStore() {
        return new JwtTokenStore(jwtTokenEnhancer());
    }

    @Bean
    protected JwtAccessTokenConverter jwtTokenEnhancer() {
        JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
        converter.setSigningKey("fanfanzhisu");
        return converter;
    }

    /**
     * Token 节点的安全策略
     * @param oauthServer
     * @throws Exception
     */
    @Override
    public void configure(AuthorizationServerSecurityConfigurer oauthServer) throws Exception {
        oauthServer
                .tokenKeyAccess("permitAll()")
                .checkTokenAccess("isAuthenticated()")
                .allowFormAuthenticationForClients()
                .passwordEncoder(NoOpPasswordEncoder.getInstance());
    }
}

5. WebSecurity 配置

  • 开放了actuator路径供健康检查
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private UserDetailsServiceImpl userServiceDetail;

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

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .csrf().disable()
                .exceptionHandling()
                .authenticationEntryPoint((request, response, authException) -> response.sendError(HttpServletResponse.SC_UNAUTHORIZED))
                .and()
                .authorizeRequests()
                .antMatchers("/actuator/**").permitAll()
                .antMatchers("/**").authenticated()
                .and()
                .httpBasic();
    }
//
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth
                .userDetailsService(userServiceDetail)
                .passwordEncoder(new BCryptPasswordEncoder());
    }


    @Bean
    public static NoOpPasswordEncoder passwordEncoder() {
        return (NoOpPasswordEncoder) NoOpPasswordEncoder.getInstance();
    }
}

Mybatis文件配置:

配置一下model,dao等文件路径:

@Configuration
@MapperScan("com.ffzs.consulauth.**.dao")
public class MybatisConfig {

    @Autowired
    private DataSource dataSource;

    @Bean
    public SqlSessionFactory sqlSessionFactory() throws Exception {
        SqlSessionFactoryBean ssfb = new SqlSessionFactoryBean();
        ssfb.setDataSource(dataSource);
        ssfb.setTypeAliasesPackage("com.ffzs.consulauth.**.model");

        PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
        ssfb.setMapperLocations(resolver.getResources("classpath*:**/generator/*.xml"));

        return ssfb.getObject();
    }
}

model,dao等文件通过idea上的generator插件生成即可。
在这里插入图片描述

重新写一下获取token时匹配用户部分逻辑

UserDetailsServiceImpl.class

  • 管理员给ROLE_ADMIN, ROLE_USER 权限
  • 普通用户只给 ROLE_USER 权限
@Service
public class UserDetailsServiceImpl implements UserDetailsService {

    @Autowired(required = false)
    private MyUserDao myUserDao;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        MyUser myUser = myUserDao.findByUsername(username);
        System.out.println(username);
        if (myUser == null) {
            throw new UsernameNotFoundException("用户不存在");
        }
        if (username.equals("admin") || username.equals("ffzs")){
            return new User(username, myUser.getPassword(), AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_ADMIN, ROLE_USER"));
        }
        return new User(username, myUser.getPassword(), AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_USER"));
    }
}

userdao要添加一个通过名字获取信息的方法:

在这里插入图片描述

在这里插入图片描述

启动类更改:

添加==@EnableDiscoveryClient==即可

在这里插入图片描述

运行&&测试

consul上注册成功
在这里插入图片描述

使用postman发送post请求:

http://localhost:8731/oauth/token?client_id=consul_server&client_secret=123456&grant_type=password&username=ffzs&password=123zxc

成功获取token

在这里插入图片描述

post请求中的参数及描述:
在这里插入图片描述

service配置

创建一个consul-service项目,用于提供登录,注册等服务:

1. 依赖:

    implementation 'org.springframework.boot:spring-boot-starter-actuator'
    implementation 'org.springframework.boot:spring-boot-starter-web'
    implementation 'org.springframework.cloud:spring-cloud-starter-openfeign:2.2.2.RELEASE'
    implementation 'org.mybatis.spring.boot:mybatis-spring-boot-starter:2.1.2'
    implementation 'org.springframework.cloud:spring-cloud-starter-consul-discovery:2.2.2.RELEASE'
    implementation 'org.springframework.cloud:spring-cloud-starter-oauth2:2.2.2.RELEASE'
    runtimeOnly 'mysql:mysql-connector-java'

2. 配置文件

bootstrap.yml:

spring:
  application:
    name: consul-service
  cloud:
    consul:
      host: localhost
      port: 8500
      discovery:
        prefer-ip-address: true
        service-name: ${spring.application.name}

feign:
  httpclient:
    enabled: true

management:
  endpoints:
    web:
      exposure:
        include: "*"
  endpoint:
    health:
      show-details: always

application.yml:

server:
  port: 8777

spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/ffzs?useUnicode=true&zeroDateTimeBehavior=convertToNull&autoReconnect=true&characterEncoding=utf-8
    username: root
    password: 123zxc

logging:
  level:
    root: INFO
    org.springframework.web: INFO
    org.springframework.security: INFO
    org.springframework.security.oauth2: INFO

3. Mybatis配置

dao,model跟uaa的一样

mybatis的配置文件跟uaa的也基本相同:

@Configuration
@MapperScan("com.ffzs.consulservice.**.dao")
public class MybatisConfig {

    @Autowired
    private DataSource dataSource;

    @Bean
    public SqlSessionFactory sqlSessionFactory() throws Exception {
        SqlSessionFactoryBean ssfb = new SqlSessionFactoryBean();
        ssfb.setDataSource(dataSource);
        ssfb.setTypeAliasesPackage("com.ffzs.consulservice.**.model");

        PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
        ssfb.setMapperLocations(resolver.getResources("classpath*:**/generator/*.xml"));

        return ssfb.getObject();
    }
}

4. Resource配置

ResourceConfiguration.class:

  • 配置了两个测试路径,user路径用于登录,不做权限限制
  • /hello/admin", "/hello/header只右ADMIN权限才能访问
@Configuration
@EnableResourceServer
public class ResourceConfiguration extends ResourceServerConfigurerAdapter {

    @Override
    public void configure(HttpSecurity http) throws Exception {
        http
                .csrf().disable()
                .authorizeRequests()
                .antMatchers("/actuator/**").permitAll()
                .antMatchers("/user/**").permitAll()
                .antMatchers("/hello/user", "hello/test").hasRole("USER")
                .antMatchers("/hello/admin", "/hello/header").hasAnyRole("ADMIN", "USER")
                .antMatchers("/**").authenticated();
    }

    /**
     * tokenServices 定义Token Service 用ResourceServerTokenservices类,配置Token是如何编码和解码的 可以用RemoteTokenServices类,即Resource Server采用远程授权服务器进行Token解码,这时也不需要配置此选项
     *
     * @param resources
     * @throws Exception
     */
    @Override
    public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
        resources
                .resourceId("server")  // 配置资源Id。
                .tokenStore(jwtTokenStore());
    }

    @Bean
    protected JwtAccessTokenConverter jwtTokenConverter() {
        JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
        converter.setSigningKey("fanfanzhisu");
        return converter;
    }

    @Bean
    public TokenStore jwtTokenStore() {
        return new JwtTokenStore(jwtTokenConverter());
    }
}

5. 注册功能

添加一个功能用于注册,service

    public int insertUser(String username, String  password){
        MyUser user=myUserDao.findByUsername(username);
        if (user != null){
            user.setPassword(encoder.encode(password));
            return myUserDao.updateByPrimaryKeySelective(user);
        }else{
            MyUser myUser=new MyUser();
            myUser.setUsername(username);
            myUser.setPassword(encoder.encode(password));
            return myUserDao.insertSelective(myUser);
        }
    }

controller:

    @PostMapping("/register")
    public String postUser(@RequestParam("username") String username , @RequestParam("password") String password){
        int back = userServiceDetail.insertUser(username,password);
        return back == 1?"注册成功":"注册失败";
    }

6. 登录功能

编写一个service通过feign向注册中心的consul-auth,也就是上面uaa发送登录请求并获取token

@FeignClient(value = "consul-auth")
public interface AuthServiceClient {

    @PostMapping(value = "/oauth/token")
    MyToken getToken(@RequestHeader("Content-Type") String content, @RequestParam("client_id") String client_id, @RequestParam("client_secret") String client_secret, @RequestParam("grant_type") String type,
                     @RequestParam("username") String username, @RequestParam("password") String password);

}

调用getToken获取token,之后装入userLoginDTO中:

    public UserLoginDTO login(String username, String password){
        MyUser user=myUserDao.findByUsername(username);
        if (null == user) {
            throw new RuntimeException("error username");
        }
        if(!encoder.matches(password,user.getPassword())){
            throw new RuntimeException("error password");
        }

        MyToken myToken =authServiceClient.getToken("application/json", "consul_server","123456","password", username, password);

        if(myToken ==null){
            throw new RuntimeException("error internal");
        }

        UserLoginDTO userLoginDTO=new UserLoginDTO();
        userLoginDTO.setMyToken(myToken);
        userLoginDTO.setUser(user);
        return userLoginDTO;
    }

用于登入的controller:

    @PostMapping("/login")
    public UserLoginDTO login(@RequestParam("username") String username , @RequestParam("password") String password){
        return userServiceDetail.login(username,password);
    }

7. 测试登录

测试注册功能,访问http://localhost:8777/user/register?username=xiaozhang&password=123456,结果如下:

在这里插入图片描述

查看数据库,xiaozhang的用户信息已经存入数据库:

在这里插入图片描述

测试登入功能,http://localhost:8777/user/login?username=xiaozhang&password=123456 ,返回token说明登录成功:

在这里插入图片描述

8. 测试权限

因为一些业务的需求会有一些端口需要鉴权,编写用于测试的controller:

@RestController
@RequestMapping("hello")
public class TestController {

    @GetMapping("user")
    public String user(){
        return "hello!!! 普通用户 !!!";
    }

    @GetMapping("admin")
    public String admin(){
        return "hello!!! 权限dog !!!!";
    }

    @GetMapping("role")
    public String test() {
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        StringBuilder res = new StringBuilder()
                .append("用户名: ").append(authentication.getName()).append("\n")
                .append("权限情况: ");
        for (Object it :authentication.getAuthorities().toArray()) {
            res.append(it).append("\t");
        }
        return res.toString();
    }

    @RequestMapping("header")
    public String header(HttpServletRequest request) {
        StringBuilder html = new StringBuilder("<table border='2' cellspacing='0'>");
        Enumeration<String> headerNames = request.getHeaderNames();
        while (headerNames.hasMoreElements()) {
            String key = headerNames.nextElement();
            html.append("<tr><td>").append(key).append("</td><td>").append(request.getHeader(key)).append("</td></tr>");
        }
        html.append("</table>");
        return html.toString();
    }
}

几个端口权限设置如下:

                .antMatchers("/hello/user", "hello/test").hasRole("USER")
                .antMatchers("/hello/admin", "/hello/header").hasAnyRole("ADMIN", "USER")
  • USER 权限可以访问 “/hello/user”, “hello/test”
  • ADMIN 都可以访问

设置token,使用postman可以直接设置,如下图,不使用postman的话可以写道header里:

在这里插入图片描述

这时我们访问,http://localhost:8777/hello/user,可以访问user;

在这里插入图片描述

访问,http://localhost:8777/hello/admin,因为没有权限,限制访问:

在这里插入图片描述

访问,http://localhost:8777/hello/role,这里我们可以看到xiaozhang只有ROLU_USER的权限:

在这里插入图片描述

更换用户,使用ffzs账号登录,更换获得token,权限都token都长了:

在这里插入图片描述

使用ffzs的账号访问http://localhost:8777/hello/role

在这里插入图片描述

ROLE_ADMIN ROLE_USER同时拥有两个权限:

这时我们试试用这个账号能否访问http://localhost:8777/hello/admin

在这里插入图片描述

有了ADMIN权限可以正常访问了。

看一下header,访问 http://localhost:8777/hello/header

在这里插入图片描述

可见token在header里的形式,如果不容postman发送请求就添加通过将"authorization":"Bearer token"添加到你的header中就可以访问了

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!