Spring Security

Spring Security

  • 开发Web应用,对页面的安全控制通常是必须的。比如:对于没有访问权限的用户需要转到登录表单页面。要实现访问控制的方法多种多样,可以通过Aop、拦截器实现,也可以通过框架实现,例如:Apache Shiro、Spring Security 。Spring Security 认证流程

    • UsernamePasswordAuthenticationFilter 实现获得用户名和密码的操作,并且只允许post请求方法
    SecurityContextPersistenceFilter extends GenericFilterBean{
    }
      
    AbstractAuthenticationProcessingFilter extends GenericFilterBean{
      
    }
    
    UsernamePasswordAuthenticationFilter extends AbstractAuthenticationProcessingFilter{
       @Override
       public Authentication attemptAuthentication(HttpServletRequest request, 
                   HttpServletResponse response) throws AuthenticationException {
          //return super.attemptAuthentication(request, response);
          if (this.postOnly && !request.getMethod().equals("POST")) {
             throw new AuthenticationServiceException("Authentication method not supported:" 
                                                      + request.getMethod());
          }
          String username = this.obtainUsername(request);
          String password = this.obtainPassword(request);
          if (username == null) {
                username = "";
          }
          if (password == null) {
                password = "";
          }
    
          username = username.trim();
          UsernamePasswordAuthenticationToken token = new 
            UsernamePasswordAuthenticationToken(username, password);
          this.setDetails(request, token);
          // 通过调用 getAuthenticationManager() 来获取 AuthenticationManager 对象,通过调用它的 authenticate 方法来查找支持该 token 认证方式的 provider,然后调用该 provider 的 authenticate 方法进行认证
          return this.getAuthenticationManager().authenticate(authRequest);
        }
    }
    
    • AuthenticationManager 接口有一个 authenticate 方法,通过该方法调用 AuthenticationProvider 的authenticate方法
    public class ProviderManager implements AuthenticationManager {
        // 维护一个AuthenticationProvider列表
        private List<AuthenticationProvider> providers = Collections.emptyList();
     
        public Authentication authenticate(Authentication authentication) 
          throws AuthenticationException {
          Class<? extends Authentication> toTest = authentication.getClass();
          AuthenticationException lastException = null;
          Authentication result = null;
          boolean debug = logger.isDebugEnabled();
          // 获得认证方式的 provider
          Iterator var6 = this.getProviders().iterator();
          //依次来认证
          while(var6.hasNext()) {
              AuthenticationProvider provider = (AuthenticationProvider)var6.next();
              if (provider.supports(toTest)) {
                  try {
                      // 如果有Authentication信息,则直接返回
                      result = provider.authenticate(authentication);
                      if (result != null) {
                          this.copyDetails(authentication, result);
                          break;
                      }
                  } catch (AccountStatusException var11) {
                      this.prepareException(var11, authentication);
                      throw var11;
                  } catch (InternalAuthenticationServiceException var12) {
                      this.prepareException(var12, authentication);
                      throw var12;
                  } catch (AuthenticationException var13) {
                      lastException = var13;
                  }
              }
            }
        }
    }
    
    • DaoAuthenticationProvider 主要完成用户授权校验,authenticate方法会调用 additionalAuthenticationChecks方法,来对请求的用户名和密码进行校验。如果校验不通过,则返回 Bad credentials
    AbstractUserDetailsAuthenticationProvider implements AuthenticationProvider{
    }
    
    DaoAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider{
      // retrieveUser方法,它返回UserDetails类
      protected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
        UserDetails loadedUser;
        try {
        //调用 UserDetailsService 对象的 loadUserByUsername这个方法;
          loadedUser = this.getUserDetailsService().loadUserByUsername(username);
        } catch (UsernameNotFoundException var6) {
            if (authentication.getCredentials() != null) {
                String presentedPassword = authentication.getCredentials().toString();
                this.passwordEncoder.isPasswordValid(this.userNotFoundEncodedPassword, 
                                                     presentedPassword, (Object)null);
            }
            throw var6;
        } catch (Exception var7) {
            throw new InternalAuthenticationServiceException(var7.getMessage(), var7);
        }
    
        if (loadedUser == null) {
            throw new InternalAuthenticationServiceException("UserDetailsService returned null, which is an interface contract violation");
        } else {
            return loadedUser;
        }
      }
      
      // additionalAuthenticationChecks方法,拿到通过用户姓名获得的该用户的信息(密码等)和用户输入的密码加密后对比,如果不正确就会报错Bad credentials的错误
      protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
        Object salt = null;
        if (this.saltSource != null) {
          //此方法在你的配置文件中去配置实现的 也是spring security加密的关键 ------划重点
            salt = this.saltSource.getSalt(userDetails);
        }
    
        if (authentication.getCredentials() == null) {
            this.logger.debug("Authentication failed: no credentials provided");
            throw new BadCredentialsException(this.messages.getMessage(
              "AbstractUserDetailsAuthenticationProvider.badCredentials", 
              "Bad credentials"));
        } else {
            String presentedPassword = authentication.getCredentials().toString();
            if (!this.passwordEncoder.isPasswordValid(userDetails.getPassword(), 
                                                      presentedPassword, salt)) {
                this.logger.debug(
                  "Authentication failed: password does not match stored value");
                throw new BadCredentialsException(this.messages.getMessage(
                  "AbstractUserDetailsAuthenticationProvider.badCredentials", 
                  "Bad credentials"));
            }
        }
      }
    }
    
    • DaoAuthenticationProvider 类中的 retrieveUser方法,是用来返回UserDetails类的,UserDetails是Spring对用户身份信息封装的一个接口。使用 UserDetailsService 类可以从特定的地方(通常是数据库)加载用户信息。
    public interface UserDetailsService {
       UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
    }
    
    public interface UserDetails extends Serializable {
        Collection<? extends GrantedAuthority> getAuthorities();
    
        String getPassword();
        String getUsername();
        boolean isAccountNonExpired();
        boolean isAccountNonLocked();
        boolean isCredentialsNonExpired();
        boolean isEnabled();
    }
    
  • Spring Boot 集成 Spring Security

    • 创建一个 demo 项目 , 可以正常访问:http://localhost:8080/hello

    • 修改 pom 文件, 添加依赖

      <dependency>
          <groupId>org.springframework.boot</groupId>
          <artifactId>spring-boot-starter-security</artifactId>
      </dependency>
      
      • 添加依赖后,整个应用就有了默认的安全机制,当再次打开URL,你将看到一个alert表单对话框,需要输入用户名和密码
      • security默认的用户名是user, 默认密码是应用启动的时候,通过UUID算法随机生成的。
    • 通过 application.propertiesm配置你可以修改默认用户名和密码

      spring.security.user.name=admin
      spring.security.user.password=123456
      
    • 使用内存用户名密码认证,自定义一个配置类 SecurityConfig , HttpSecurity 方法介绍

      @Configuration
      @EnableWebSecurity
      class WebSecurityConfig extends WebSecurityConfigurerAdapter {
          // 在任何应用中,并不是所有请求都需要同等程度地保护起来。有些请求需要认证,有些则不需要。
          // 对每个请求进行细粒度安全性控制的关键在于重载configure(HttpSecurity)方法
          @Override
          protected void configure(HttpSecurity http) throws Exception {  
            // 默认配置, 父类中的实现 : super.configure(http);
            http.authorizeRequests()
                .anyRequest().authenticated()  // 所有请求需要身份认证
                .and().formLogin()
                .and().httpBasic();
            // 
            http.authorizeRequests()  // 对请求进行认证
                .antMatchers("/", "/hello").permitAll()
                .antMatchers("/admin/**").hasRole("ADMIN")   // 角色检查
                .antMatchers("/user/**").hasRole("USER")   // 角色检查
                .antMatchers("/**").hasAnyRole("ADMIN", "USER")
                .and().formLogin()
                .and().logout().logoutSuccessUrl("/login").permitAll()
                .and().csrf().disable();   // 关闭csrf验证
          }
      
          @Override
          protected void configure(AuthenticationManagerBuilder auth) throws Exception {
              // 使用内存用户名密码认证
              auth.inMemoryAuthentication()
                .withUser("user").password(passwordEncoder().encode("1234")).roles("USER")
                .and().withUser("admin").password(passwordEncoder().encode("admin"))
                .roles("ADMIN", "USER");
          }
        
          @Bean
          public PasswordEncoder passwordEncoder() {
              return new BCryptPasswordEncoder();
          }
      }
      
  • 用数据库存储用户和角色,实现安全认证

    • 数据库层设计:新建三张表User,Role,UserRole

      @Entity
      class User {
        private Integer id;
        private String userName;
        private String password;
      }
      
      @Entity
      class Role {
        private Integer id;
        private String role;
      }
      
      @Entity
      class UserRole {
        private Integer id;
        private Integer userId;
        private Integer roleId;
      }
      
    • 配置类 SecurityConfig

      @Configuration
      @EnableWebSecurity
      public class WebSecurityConfig extends WebSecurityConfigurerAdapter{
          @Override
          @Bean
          public UserDetailsService userDetailsService() { //覆盖写userDetailsService方法
              return new CustomUserDetailService();
          }
        
          @Override
          protected void configure(HttpSecurity http) throws Exception { 
              http.authorizeRequests()  // 对请求进行认证
                .antMatchers("/", "/hello").permitAll()
                .antMatchers("/admin/**").hasAuthority("ADMIN") // 授权检查
                .antMatchers("/user/**").hasAuthority("USER")   // 授权检查
                .antMatchers("/**").hasAnyAuthority("ADMIN","USER")
                .and().formLogin()
                .and().logout().logoutSuccessUrl("/login").permitAll()
                .and().csrf().disable();   // 关闭csrf验证
          }
          @Override
          protected void configure(AuthenticationManagerBuilder auth) throws Exception {
              auth.userDetailsService(userDetailsService()); //从数据库获取用户和角色
          }
      }
      
    • 实现 UserDetailsService 子类

      public class CustomUserDetailService implements UserDetailsService {
        @Autowired
         UserDao userDao;
         @Autowired
         RoleDao roleDao;
        
        @Override
        public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
          User user = userDao.getUserByUsername(username);
      		ArrayList<String> userRoles = roleDao.listByUserId(user.getId());
      
          List<SimpleGrantedAuthority> list = new ArrayList<SimpleGrantedAuthority>();
          for (String roleName : userRoles) {
            list.add(new SimpleGrantedAuthority(roleName));
          }
          return new org.springframework.security.core.userdetails.User(
            user.getUserName(), user.getPassword(), list);
        }
      }
      
    • 自定义登录页面 Spring Security 5 Login Form Example

  • Spring Security 和 JWT 整合

    • 修改pom 文件

      <dependency>
          <groupId>org.springframework.boot</groupId>
          <artifactId>spring-boot-starter-security</artifactId>
      </dependency>
      <dependency>
          <groupId>io.jsonwebtoken</groupId>
          <artifactId>jjwt</artifactId>
          <version>0.7.0</version>
      </dependency>
      
    • 添加配置类

      @Configuration
      @EnableWebSecurity
      class WebSecurityConfig extends WebSecurityConfigurerAdapter {
      
          // 设置 HTTP 验证规则
          @Override
          protected void configure(HttpSecurity http) throws Exception {
            
            http.authorizeRequests()  // 对请求进行认证
                .antMatchers("/hello").permitAll()  // 不需要身份认证
                .antMatchers("/admin/**").hasRole("ADMIN")   // 角色检查
                .antMatchers("/**").hasAnyRole("ADMIN", "USER")
                .antMatchers("/hello").hasAuthority("AUTH_WRITE")  // 权限检查
                .and().formLogin()
                .and().logout().logoutSuccessUrl("/login").permitAll()
                .and().csrf().disable();   // 关闭csrf验证
                // 在UsernamePasswordAuthenticationFilter 之前添加一个过滤器(让所有 /login 的请求都经过 JWTLoginFilter 过滤器,进行登录校验,验证成功后,生成JWT 返回)
                .and().addFilterBefore(new JWTLoginFilter("/login", authenticationManager()), UsernamePasswordAuthenticationFilter.class)
                // 在UsernamePasswordAuthenticationFilter 之前添加一个过滤器(让所有请求都经过JWTAuthenticationFilter 过滤器,从JWT中取出username和authentication,并生成Token,验证Token是否合法)
                .addFilterBefore(new JWTAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
          }
      
          @Override
          protected void configure(AuthenticationManagerBuilder auth) throws Exception {
              // 使用自定义的身份验证组件,代替默认的 DaoAuthenticationProvider
              auth.authenticationProvider(new CustomAuthenticationProvider());
          }
      }
      
  • Spring Boot Security 整合 OAuth2 设计安全API接口服务

    • oauth2 知识介绍

      • oauth2授权主要由两部分组成:认证服务(Authorization server)和 资源服务(Resource server)
      • oauth2 主要支持4种模式4种 grant_type
        • authorization_code :授权码模式,即先登录获取code,再获取token
        • client_credentials :客户端模式,用户向客户端注册,然后客户端以自己的名义向’服务端’获取资源,无用户
        • password : 密码模式,将用户名 密码传过去,直接获取token
        • refresh_token :刷新access_token
    • 建表:Spring OAuth2 己经设计好了数据库的表,且不可变。表及字段说明参照:Oauth2数据库表说明

    • pom 文件

      <dependency>
          <groupId>org.springframework.boot</groupId>
          <artifactId>spring-boot-starter-oauth2-client</artifactId>
      </dependency>
      <dependency>
          <groupId>org.springframework.boot</groupId>
          <artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
      </dependency>
      <dependency>
          <groupId>org.springframework.security.oauth</groupId>
          <artifactId>spring-security-oauth2</artifactId>
          <version>2.3.8.RELEASE</version>
      </dependency>
      
    • 新增 AuthorizationServerConfiguration 配置类

      @Configuration
      @EnableAuthorizationServer
      public class AuthorizationServerConfiguration extends AuthorizationServerConfigurerAdapter {
          // 注入authenticationManager 来支持 password grant type
          @Autowired
          private AuthenticationManager authenticationManager;
        
          // 配置内存存储 token
          @Bean
          public TokenStore tokenStore() {
              return new InMemoryTokenStore();
          }
      
          @Override
          public void configure(AuthorizationServerSecurityConfigurer security) 
            throws Exception {
              // 配置oauth2服务跨域
              CorsConfigurationSource source = new CorsConfigurationSource() {
                  @Override
                  public CorsConfiguration getCorsConfiguration(HttpServletRequest request) {
                      CorsConfiguration corsConfiguration = new CorsConfiguration();
                      corsConfiguration.addAllowedHeader("*");
                      corsConfiguration.addAllowedOrigin(request.getHeader( HttpHeaders.ORIGIN));
                      corsConfiguration.addAllowedMethod("*");
                      corsConfiguration.setAllowCredentials(true);
                      corsConfiguration.setMaxAge(3600L);
                      return corsConfiguration;
                  }
              };
              // 意思是 /oauth/token 路径 不需要授权就可以访问
              security.tokenKeyAccess("permitAll()")
                      .checkTokenAccess("permitAll()")
                      .allowFormAuthenticationForClients()
                      .addTokenEndpointAuthenticationFilter(new CorsFilter(source));
          }
      
          @Override
          public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
              // 为了测试方便,我们先插入一条客户端信息
              clients.inMemory().withClient("dev").secret("dev").scopes("app")
                      .authorizedGrantTypes("client_credentials","authorization_code",
                                            "password","refresh_token")
                      .redirectUris("http://www.baidu.com")
                      .refreshTokenValiditySeconds(360).accessTokenValiditySeconds(360)
                      .autoApprove(false);
          }
      
          @Override
          public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
              endpoints.tokenStore(tokenStore())
                // 配置内存存储 token , 使用authenticationManager 来支持 password grant type
                .authenticationManager(authenticationManager);
          }
      }
      
    • 新增 ResourceServerConfig

      // 配置 OAuth2 管理的资源,和 WebSecurityConfig 授权配置一样
      @Configuration
      @EnableResourceServer
      public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
          @Override
          public void configure(HttpSecurity http) throws Exception {
              http.requestMatchers().antMatchers("/hi")
                      .and()
                      .authorizeRequests()
                      .antMatchers("/hi").authenticated();
          }
      }
      
    • 修改 WebSecurityConfig , 支持 password 模式

      @Configuration
      @EnableWebSecurity
      public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
          @Override
          protected void configure(HttpSecurity http) throws Exception {
      
              http.authorizeRequests()
                      .antMatchers("/", "/hello").permitAll()
                      .antMatchers("/**").authenticated()
      //                .anyRequest().authenticated()
                      .and().formLogin()
                      .and().httpBasic();
          }
        
          @Override
          protected void configure(AuthenticationManagerBuilder auth) throws Exception {
              auth.inMemoryAuthentication()
                .withUser("user")
                .password(passwordEncoder().encode("1234"))
                .roles("USER");
          }
        
          // 支持 OAuth2 的密码编码方式, 因为OAuth2 不支持 BCrypt
          @Bean
          public PasswordEncoder passwordEncoder() {
              return new PasswordEncoder() {
                  @Override
                  public String encode(CharSequence charSequence) {
                      return charSequence.toString();
                  }
      
                  @Override
                  public boolean matches(CharSequence charSequence, String s) {
                      return Objects.equals(charSequence.toString(),s);
                  }
              };
          }
        
          // 需要配置这个支持password模式 , support password grant type
          @Override
          @Bean
          public AuthenticationManager authenticationManagerBean() throws Exception {
              return super.authenticationManagerBean();
          }
      }
      
    • 测试: 密码授权模式、刷新 token 模式 、客户端授权模式

      #密码授权模式
      curl -X POST -d "username=user&password=1234&grant_type=password&client_id=dev&client_secret=dev" http://localhost:8080/oauth/token
      
      {"error":"unsupported_grant_type","error_description":"Unsupported grant type"}
      
      {"access_token":"b541cb47-aab8-48b5-b845-dfe002e4dbe3","token_type":"bearer","refresh_token":"8cfad00a-3ce0-4925-bb8c-bf52725d1fec","expires_in":359,"scope":"app"}
      
      # 刷新 token 模式
      curl -X POST -d "grant_type=refresh_token&refresh_token=8cfad00a-3ce0-4925-bb8c-bf52725d1fec&client_id=dev&client_secret=dev" http://localhost:8080/oauth/token
      
      {"error":"server_error","error_description":"Internal Server Error"} # 意思是 UserDetailsService is required
      
      #客户端授权模式
      curl -X POST -d "grant_type=client_credentials&client_id=dev&client_secret=dev" http://localhost:8080/oauth/token
      
      {"access_token":"598ab278-b5f0-484b-bdc6-ff01199c3631","token_type":"bearer","expires_in":359,"scope":"app"}
      
      # 访问 URL
      curl http://localhost:8080/hi\?name\=zhangsan\&access_token\=b541cb47-aab8-48b5-b845-dfe002e4dbe3
      
    • 测试授权码模式

      http://localhost:8080/oauth/authorize?response_type=code&client_id=dev&redirect_uri=http://www.baidu.com
      

    • 其他

      • SpringBoot + Spring Security OAuth2基本使用 运行不起来,报错:Field configurers in org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerSecurityConfiguration required a bean of type ‘java.util.List’ that could not be found
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值