spring security工做流程

1.攔截登陸,獲取到用戶的全部權限信息

1.1默認的攔截器攔截 /login 方法

  • OncePerRequestFilter 集成這個過濾器,每一次請求都將會被攔截過濾java

  • 默認使用 UsernamePasswordAuthenticationFilter 來攔截用戶的login操做。可是獲取的用戶名、密碼默認是經過formdata傳遞過來的(相似於get方式,數據跟在連接後面)。可是對於json的請求參數就獲取不到了,因此須要改寫這個類spring

    • public class UsernamePasswordAuthenticationFilter extends
      		AbstractAuthenticationProcessingFilter {  
        public Authentication attemptAuthentication(HttpServletRequest request,
      			HttpServletResponse response) throws AuthenticationException {
      		if (postOnly && !request.getMethod().equals("POST")) {
      			throw new AuthenticationServiceException(
      					"Authentication method not supported: " + 				request.getMethod());
      		}
      		//從request中獲取到用戶名
      		String username = obtainUsername(request);
          	//獲取密碼
      		String password = obtainPassword(request);
      
      		if (username == null) {
      			username = "";
      		}
      		if (password == null) {
      			password = "";
      		}
      		username = username.trim();
      
      		UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
      				username, password);
      
      		// Allow subclasses to set the "details" property
      		setDetails(request, authRequest);
      
      		return this.getAuthenticationManager().authenticate(authRequest);
      	}
      
      
      }

1.2 自定義攔截方法

public class MyUsernamePasswordAuthenticationFilter extends UsernamePasswordAuthenticationFilter {

攔截登陸數據庫

/**
     * 父類的方法獲取用戶名,密碼的方式是從request.getParameter中得到
     * @param request
     * @param response
     * @return
     * @throws AuthenticationException
     */
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
  //獲取request的請求體
  String param = this.getRequestBody(request);
  String userName = null;
  String password = null;
  if (!StringUtils.isEmpty(param)) {
    JSONObject paraJson = JSONObject.fromObject(param);
    userName = paraJson.getString("username");
    password = paraJson.getString("password");
  }

  if (!StringUtils.isEmpty(userName) && !StringUtils.isEmpty(password)) {
    userName = userName.trim();

    UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
      userName, password);

    // Allow subclasses to set the "details" property
    setDetails(request, authRequest);
    List<AuthenticationProvider> providers = new ArrayList<>();
    //這裏必須將注入的provider添加進來,否則會爆NOP
    providers.add(daoAuthenticationProvider);
    this.setAuthenticationManager(new ProviderManager(providers));
    //調用父類的驗證方法
    return this.getAuthenticationManager().authenticate(authRequest);
  }
  return null;
}
/**
     * 獲取請求體中的內容
     * @param request
     * @return
     */
private String getRequestBody(HttpServletRequest request) {
  BufferedReader br = null;
  StringBuilder sb = new StringBuilder("");
  try
  {
    br = request.getReader();
    String str;
    while ((str = br.readLine()) != null)
    {
      sb.append(str);
    }
    br.close();
  }
  catch (IOException e)
  {
    e.printStackTrace();
  }
  finally
  {
    if (null != br)
    {
      try
      {
        br.close();
      }
      catch (IOException e)
      {
        e.printStackTrace();
      }
    }
  }
  return sb.toString();

}

須要自定義註解起做用的話必須加上 http.addFilterAt(new MyUsernamePasswordAuthenticationFilter(daoAuthenticationProvider()), UsernamePasswordAuthenticationFilter.class); 代碼,將自定義的攔截過濾器替換掉默認的。必須傳一個AuthenticationProvider()過去,否則就會爆NOPjson

@Override
protected void configure(HttpSecurity http) throws Exception {
  http
    .authorizeRequests()
   
    .authorizeRequests().anyRequest().authenticated()
    //.and()
    //   .formLogin().loginPage("/login")
    //設置默認登陸成功跳轉頁面
    //.defaultSuccessUrl("/index", true)
    //.successHandler(myAuthenticationSuccessHandler)
    //                    .permitAll()
    .and()
    .logout()
    .permitAll()
    .and()
    .csrf() //CSRF(Cross-site request forgery)跨站請求僞造
    .disable()
    ;
  //默認是調用 內置的UsernamePasswordAuthenticationFilter,而它接收參數只是從相似get方式傳遞的參數。而requestBody彷佛就
  // 獲取不到。這個方法是將自定義的過濾器來替換掉security默認的過濾器的做用
  http.addFilterAt(new MyUsernamePasswordAuthenticationFilter(daoAuthenticationProvider()), UsernamePasswordAuthenticationFilter.class);
}

1.3 攔截器的工做原理

通過一系列的過濾攔截跑到了自定義的過濾器方法中app

經過調用各個authenticate方法最後代理到自定義的UserDetailService來校驗用戶是否可以登陸成功和拿到用戶的全部權限信息ide

調用自定義post

AbstractUserDetailsAuthenticationProvider
try {
		preAuthenticationChecks.check(user);
		//校驗密碼是否正確以前注入的provider(DaoAuthenticationProvider)
		additionalAuthenticationChecks(user,
					(UsernamePasswordAuthenticationToken) authentication);
	}
	
@SuppressWarnings("deprecation")
protected void additionalAuthenticationChecks(UserDetails userDetails,
                                              UsernamePasswordAuthenticationToken authentication)
  throws AuthenticationException {
  if (authentication.getCredentials() == null) {
    logger.debug("Authentication failed: no credentials provided");

    throw new BadCredentialsException(messages.getMessage(
      "AbstractUserDetailsAuthenticationProvider.badCredentials",
      "Bad credentials"));
  }

  //請求登陸時傳遞過來的密碼
  String presentedPassword = authentication.getCredentials().toString();
  //判斷請求傳遞過來的密碼與數據庫中查詢出來的是否匹配
  // userDetails.getPassword()數據庫中查詢出來的而且已經經過某種指定的加密方式加密的密碼
  //passwordEncoder.matches(明文,密文)經過某種解密方式解密密文看獲得的明文是否與方法中的明文相匹配
  if (!passwordEncoder.matches(presentedPassword, userDetails.getPassword())) {
    logger.debug("Authentication failed: password does not match stored value");

    throw new BadCredentialsException(messages.getMessage(
      "AbstractUserDetailsAuthenticationProvider.badCredentials",
      "Bad credentials"));
  }
}
  • 自定義的UserDetailServiceui

    設置的密碼信息必須是要跟注入的DaoAuthenticationProvider使用的加密方式一致,不然等登陸的時候密碼校驗通不過this

@Service
public class CustUserDetailService implements UserDetailsService {
    @Autowired
    private IUserRoleService userRoleService;
    @Autowired
    private IUserService userService;

    @Override
    public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
        User user = userService.getUserByUserCode(s);
        Assert.isTrue(null != user, "用戶不存在");
        //查詢該用戶的對應權限
        List<SimpleGrantedAuthority> authorities = new ArrayList<>();
        List<UserRole> roleList = this.userRoleService.getRoleList(s);
        if (!CollectionUtils.isEmpty(roleList)) {
            roleList.forEach(userRole -> authorities.add(
                    new SimpleGrantedAuthority(userRole.getRoleCode())
            ));
        }

        AuthUser authUser = new AuthUser(s, user.getUserPwd(), authorities);
        authUser.setId(user.getId());
        authUser.setNickname(user.getUserName());
      	//設置的密碼信息必須是要跟注入的DaoAuthenticationProvider使用的加密方式一致,不然等登陸的時候密碼校驗通不過
        return new org.springframework.security.core.userdetails.User(user.getUserName(),
                new BCryptPasswordEncoder().encode(user.getUserPwd()), authorities);
    }
}
  • 關鍵的bean注入
//注入 DaoAuthenticationProvider 給providerManager
    @Bean
    public DaoAuthenticationProvider daoAuthenticationProvider() {
        DaoAuthenticationProvider daoAuthenticationProvider = new DaoAuthenticationProvider();
        //設置對應的密碼加密方式。因此要在對應的UserDetailService上面使用這種加密方式給密碼加密,不然最後會登陸不上
        daoAuthenticationProvider.setPasswordEncoder(new BCryptPasswordEncoder());
        daoAuthenticationProvider.setUserDetailsService(custUserDetailService());
        return daoAuthenticationProvider;
    }

    @Bean
    @Override
    protected AuthenticationManager authenticationManager() throws Exception {
        return super.authenticationManager();
    }

    @Autowired//注意這個方法是注入的
    public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(custUserDetailService());
    }
@EnableWebSecurity
//啓用@preAuth註解,角色編碼必須以 ROLE_開頭
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter{

    @Bean
    public CustUserDetailService custUserDetailService(){
        return new CustUserDetailService();
    }

    @Override
    protected void configure(AuthenticationManagerBuilder authenticationManagerBuilder) throws Exception {

        authenticationManagerBuilder.userDetailsService(custUserDetailService());
    }

    /**
     *   1.首先當咱們要自定義Spring Security的時候咱們須要繼承自WebSecurityConfigurerAdapter來完成,相關配置重寫對應 方法便可。
         2.咱們在這裏註冊CustomUserService的Bean,而後經過重寫configure方法添加咱們自定義的認證方式。
         3.在configure(HttpSecurity http)方法中,咱們設置了登陸頁面,並且登陸頁面任何人均可以訪問,而後設置了登陸失敗地址,也設置了註銷請求,註銷請求也是任何人均可以訪問的。
         4.permitAll表示該請求任何人均可以訪問,.anyRequest().authenticated(),表示其餘的請求都必需要有權限認證。
         5.這裏咱們能夠經過匹配器來匹配路徑,好比antMatchers方法,假設我要管理員才能夠訪問admin文件夾下的內容,我能夠這樣來寫:.antMatchers("/admin/**").hasRole("ROLE_ADMIN"),也能夠設置admin文件夾下的文件能夠有多個角色來訪問,寫法以下:.antMatchers("/admin/**").hasAnyRole("ROLE_ADMIN","ROLE_USER")
         6.能夠經過hasIpAddress來指定某一個ip能夠訪問該資源,假設只容許訪問ip爲210.210.210.210的請求獲取admin下的資源,寫法以下.antMatchers("/admin/**").hasIpAddress("210.210.210.210")
         7.更多的權限控制方式參看下表:
     * @param http
     * @throws Exception
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                    .authorizeRequests().anyRequest().authenticated()
              
                .and()
                    .logout()
                    .permitAll()
                .and()
                    .csrf() //CSRF(Cross-site request forgery)跨站請求僞造
                        .disable()
                ;
        //默認是調用 內置的UsernamePasswordAuthenticationFilter,而它接收參數只是從相似get方式傳遞的參數。而requestBody彷佛就
        // 獲取不到。這個方法是將自定義的過濾器來替換掉security默認的過濾器的做用
          http.addFilterAt(new MyUsernamePasswordAuthenticationFilter(daoAuthenticationProvider()), UsernamePasswordAuthenticationFilter.class);
    }
}
  • 限制方法訪問權限編碼

    • 添加註解

    • **@EnableWebSecurity **

    • @EnableGlobalMethodSecurity(prePostEnabled = true)

    • /*
      which means that access will only be allowed for users with the role "ROLE_USER". Obviously the same thing could easily be achieved using a traditional configuration and a simple configuration attribute for the required role. But what about:
      */
      //如上,須要定義的角色編碼爲 ROLE_ 開頭
      @PreAuthorize("hasRole('USER')")
      public void create(Contact contact);
      @GetMapping("/index")
      //說明擁有角色 ROLE_admin角色
      @PreAuthorize("hasRole('admin')")
      public Map<String, Object> index() {
        Map<String, Object> map = new HashMap<>(2);
      
        map.put("success", true);
        map.put("time", System.currentTimeMillis());
        return map;
      }

1.4攔截登陸流程

登陸代碼

String userCode = json.getString("username");
String pwd = json.getString("password");
UsernamePasswordAuthenticationToken upToken = new UsernamePasswordAuthenticationToken(userCode, pwd);
//authenticationManager 注入的是 ProviderManager
final Authentication authentication = authenticationManager.authenticate(upToken);
SecurityContextHolder.getContext().setAuthentication(authentication);

// AbstractUserDetailsAuthenticationProvider

public Authentication authenticate(Authentication authentication)
      throws AuthenticationException {
   Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication,
         messages.getMessage(
               "AbstractUserDetailsAuthenticationProvider.onlySupports",
               "Only UsernamePasswordAuthenticationToken is supported"));

   // Determine username
   String username = (authentication.getPrincipal() == null) ? "NONE_PROVIDED"
         : authentication.getName();

   boolean cacheWasUsed = true;
   UserDetails user = this.userCache.getUserFromCache(username);

   if (user == null) {
      cacheWasUsed = false;

      try {
        // retrieveUser調用注入的具體的UserDetailService,獲取用戶權限信息
         user = retrieveUser(username,
               (UsernamePasswordAuthenticationToken) authentication);
      }
      catch (UsernameNotFoundException notFound) {
         logger.debug("User '" + username + "' not found");

         if (hideUserNotFoundExceptions) {
            throw new BadCredentialsException(messages.getMessage(
                  "AbstractUserDetailsAuthenticationProvider.badCredentials",
                  "Bad credentials"));
         }
         else {
            throw notFound;
         }
      }

      Assert.notNull(user,
            "retrieveUser returned null - a violation of the interface contract");
   }

   try {
      preAuthenticationChecks.check(user);
      additionalAuthenticationChecks(user,
            (UsernamePasswordAuthenticationToken) authentication);
   }
   catch (AuthenticationException exception) {
      if (cacheWasUsed) {
         // There was a problem, so try again after checking
         // we're using latest data (i.e. not from the cache)
         cacheWasUsed = false;
         user = retrieveUser(username,
               (UsernamePasswordAuthenticationToken) authentication);
         preAuthenticationChecks.check(user);
         additionalAuthenticationChecks(user,
               (UsernamePasswordAuthenticationToken) authentication);
      }
      else {
         throw exception;
      }
   }

   postAuthenticationChecks.check(user);

   if (!cacheWasUsed) {
      this.userCache.putUserInCache(user);
   }

   Object principalToReturn = user;

   if (forcePrincipalAsString) {
      principalToReturn = user.getUsername();
   }

   return createSuccessAuthentication(principalToReturn, authentication, user);
}

調用對應的UserDetailService的loadUserByUsername 獲取權限信息

protected final UserDetails retrieveUser(String username,
      UsernamePasswordAuthenticationToken authentication)
      throws AuthenticationException {
   prepareTimingAttackProtection();
   try {
      UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
      if (loadedUser == null) {
         throw new InternalAuthenticationServiceException(
               "UserDetailsService returned null, which is an interface contract violation");
      }
      return loadedUser;
   }
   catch (UsernameNotFoundException ex) {
      mitigateAgainstTimingAttack(authentication);
      throw ex;
   }
   catch (InternalAuthenticationServiceException ex) {
      throw ex;
   }
   catch (Exception ex) {
      throw new InternalAuthenticationServiceException(ex.getMessage(), ex);
   }
}

獲取到權限信息

//user: 從數據庫查出的用戶信息
// authentication :登陸傳過來組裝的用戶信息
// 判斷密碼是否正確
additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication);
AbstractUserDetailsAuthenticationProvider.authenticate

校驗密碼是否正確

相關文章
相關標籤/搜索