1. 程式人生 > >Spring Security之用戶名+密碼登錄

Spring Security之用戶名+密碼登錄

查找 prot value MLOG odi 時間 lte iss ada

自定義用戶認證邏輯

處理用戶信息獲取邏輯

實現UserDetailsService接口

@Service
public class MyUserDetailsService implements UserDetailsService {
    private Logger logger = LoggerFactory.getLogger(getClass());

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        logger.info("根據用戶名查找用戶信息,登錄用戶名:" + username);
        // 從數據庫查詢相關的密碼和權限,這裏返回一個假的數據
        // 用戶名,密碼,權限
        return new User(username,
                        "123456",
                        AuthorityUtils.commaSeparatedStringToAuthorityList("admin"));
    }
}

處理用戶校驗邏輯

UserDetails接口的一些方法,封裝了登錄時的一些信息

public interface UserDetails extends Serializable {   
   /** 權限信息
    * Returns the authorities granted to the user. Cannot return <code>null</code>.
    *
    * @return the authorities, sorted by natural key (never <code>null</code>)
    */
   Collection<? extends GrantedAuthority> getAuthorities();

   /** 密碼
    * Returns the password used to authenticate the user.
    *
    * @return the password
    */
   String getPassword();

   /** 登錄名
    * Returns the username used to authenticate the user. Cannot return <code>null</code>
    * .
    *
    * @return the username (never <code>null</code>)
    */
   String getUsername();

   /** 賬戶是否過期
    * Indicates whether the user's account has expired. An expired account cannot be
    * authenticated.
    *
    * @return <code>true</code> if the user's account is valid (ie non-expired),
    * <code>false</code> if no longer valid (ie expired)
    */
   boolean isAccountNonExpired();

   /** 賬戶是否被鎖定(凍結)
    * Indicates whether the user is locked or unlocked. A locked user cannot be
    * authenticated.
    *
    * @return <code>true</code> if the user is not locked, <code>false</code> otherwise
    */
   boolean isAccountNonLocked();

   /** 密碼是否過期
    * Indicates whether the user's credentials (password) has expired. Expired
    * credentials prevent authentication.
    *
    * @return <code>true</code> if the user's credentials are valid (ie non-expired),
    * <code>false</code> if no longer valid (ie expired)
    */
   boolean isCredentialsNonExpired();

   /** 賬戶是否可用(刪除)
    * Indicates whether the user is enabled or disabled. A disabled user cannot be
    * authenticated.
    *
    * @return <code>true</code> if the user is enabled, <code>false</code> otherwise
    */
   boolean isEnabled();
}

返回數據寫成

return new User(username, // 用戶名
                "123456", // 密碼
                true, // 是否可用
                true, // 賬號是否過期
                true, // 密碼是否過期
                true, // 賬號沒有被鎖定標誌
                AuthorityUtils.commaSeparatedStringToAuthorityList("admin"));

處理密碼加密解密

PasswordEncoder接口

public interface PasswordEncoder {

    /** 加密
     * Encode the raw password. Generally, a good encoding algorithm applies a SHA-1 or
     * greater hash combined with an 8-byte or greater randomly generated salt.
     */
    String encode(CharSequence rawPassword);

    /** 判斷密碼是否匹配
     * Verify the encoded password obtained from storage matches the submitted raw
     * password after it too is encoded. Returns true if the passwords match, false if
     * they do not. The stored password itself is never decoded.
     *
     * @param rawPassword the raw password to encode and match
     * @param encodedPassword the encoded password from storage to compare with
     * @return true if the raw password, after encoding, matches the encoded password from
     * storage
     */
    boolean matches(CharSequence rawPassword, String encodedPassword);

}

在BrowerSecurityConfig中配置PasswordEncoder

// 配置PasswordEncoder
@Bean
public PasswordEncoder passwordEncoder() {
    return new BCryptPasswordEncoder();
}

MyUserDetailsService.java改成

// 註入passwordEncoder
@Autowired
private PasswordEncoder passwordEncoder;

// 返回寫成這樣
return new User(username, // 用戶名
                passwordEncoder.encode("123456"), // 這個是從數據庫中讀取的已加密的密碼
                true, // 是否可用
                true, // 賬號是否過期
                true, // 密碼是否過期
                true, // 賬號沒有被鎖定標誌
                AuthorityUtils.commaSeparatedStringToAuthorityList("admin"));

個性化用戶認證流程

自定義登錄頁面

修改BrowserSecurityConfig類

@Configuration
public class BrowserSecurityConfig extends WebSecurityConfigurerAdapter {
    // 配置PasswordEncoder
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        System.out.println("BrowserSecurityConfig");
        http.formLogin() // 表單登錄
                .loginPage("/sign.html") //  自定義登錄頁面URL
                .loginProcessingUrl("/authentication/form") // 處理登錄請求的URL
                .and()
                .authorizeRequests() // 對請求做授權
                .antMatchers("/sign.html").permitAll() // 登錄頁面不需要認證
                .anyRequest() // 任何請求
                .authenticated() // 都需要身份認證
                .and().csrf().disable(); // 暫時將防護跨站請求偽造的功能置為不可用
    }
}

問題

  1. 不同的登錄方式,通過頁面登錄,通過app登錄
  2. 給多個應用提供認證服務,每個應用需要的自定義登錄頁面

技術分享圖片

@Configuration
public class BrowserSecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    private SecurityProperties securityProperties;

    // 配置PasswordEncoder
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        System.out.println("BrowserSecurityConfig");
        http.formLogin() // 表單登錄
                .loginPage("/authentication/require") //  自定義登錄頁面URL
                .loginProcessingUrl("/authentication/form") // 處理登錄請求的URL
                .and()
                .authorizeRequests() // 對請求做授權
                .antMatchers("/authentication/require",
                        securityProperties.getBrowser().getLoginPage())
                    .permitAll() // 登錄頁面不需要認證
                .anyRequest() // 任何請求
                .authenticated() // 都需要身份認證
                .and().csrf().disable(); // 暫時將防護跨站請求偽造的功能置為不可用
    }
}

BrowserSecurityController判斷訪問的url如果以.html結尾就跳轉到登錄頁面,否則就返回json格式的提示信息

@RestController
public class BrowserSecurityController {
    private Logger logger = LoggerFactory.getLogger(getClass());

    private RequestCache requestCache = new HttpSessionRequestCache();

    private RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();

    @Autowired
    private SecurityProperties securityProperties;

    /**
     * 需要身份認證時,跳轉到這裏
     *
     * @param request
     * @param response
     * @return
     */
    @RequestMapping("/authentication/require")
    @ResponseStatus(code = HttpStatus.UNAUTHORIZED)
    public SimpleResponse requireAuthentication(HttpServletRequest request,
                                        HttpServletResponse response)
            throws IOException {
        SavedRequest savedRequest = requestCache.getRequest(request, response);
        if (savedRequest != null) {
            String targetUrl = savedRequest.getRedirectUrl();
            logger.info("引發跳轉請求的url是:" + targetUrl);
            if (StringUtils.endsWithIgnoreCase(targetUrl, ".html")) {
                redirectStrategy.sendRedirect(request, response,
                        securityProperties.getBrowser().getLoginPage());
            }
        }
        return new SimpleResponse("訪問的服務需要身份認證,請引導用戶到登錄頁");
    }
}

自定義登錄成功處理

AuthenticationSuccessHandler接口,此接口登錄成功後會被調用

@Component
public class ImoocAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
    private Logger logger = LoggerFactory.getLogger(ImoocAuthenticationSuccessHandler.class);

    @Autowired
    private ObjectMapper objectMapper;

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request,
                                        HttpServletResponse response,
                                        Authentication authentication)
            throws IOException, ServletException {
        logger.info("登錄成功");
        // 登錄成功後把authentication返回給前臺
        response.setContentType("application/json;charset=UTF-8");
        response.getWriter().write(objectMapper.writeValueAsString(authentication));
    }
}

技術分享圖片

自定義登錄失敗處理

@Component
public class ImoocAuthenticationFailHandler implements AuthenticationFailureHandler  {
    private Logger logger = LoggerFactory.getLogger(getClass());

    @Autowired
    private ObjectMapper objectMapper;

    @Override
    public void onAuthenticationFailure(HttpServletRequest request,
                                        HttpServletResponse response,
                                        AuthenticationException e)
            throws IOException, ServletException {
        logger.info("登錄失敗");
        response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
        response.setContentType("application/json;charset=UTF-8");
        response.getWriter().write(objectMapper.writeValueAsString(e));
    }
}

技術分享圖片

問題

  • 登錄成功或失敗後返回頁面還是json數據格式

登錄成功後的處理

@Component
public class ImoocAuthenticationSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {
    private Logger logger = LoggerFactory.getLogger(ImoocAuthenticationSuccessHandler.class);

    @Autowired
    private ObjectMapper objectMapper;

    @Autowired
    private SecurityProperties securityProperties;

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request,
                                        HttpServletResponse response,
                                        Authentication authentication)
            throws IOException, ServletException {
        logger.info("登錄成功");
        if (LoginType.JSON.equals(securityProperties.getBrowser().getLoginType())) {
            // 登錄成功後把authentication返回給前臺
            response.setContentType("application/json;charset=UTF-8");
            response.getWriter().write(objectMapper.writeValueAsString(authentication));
        } else {
            super.onAuthenticationSuccess(request, response, authentication);
        }
    }
}

登錄失敗後的處理

@Component
public class ImoocAuthenticationFailHandler extends SimpleUrlAuthenticationFailureHandler {
    private Logger logger = LoggerFactory.getLogger(getClass());

    @Autowired
    private ObjectMapper objectMapper;

    @Autowired
    private SecurityProperties securityProperties;

    @Override
    public void onAuthenticationFailure(HttpServletRequest request,
                                        HttpServletResponse response,
                                        AuthenticationException e)
            throws IOException, ServletException {
        logger.info("登錄失敗");
        if (LoginType.JSON.equals(securityProperties.getBrowser().getLoginType())) {
            response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
            response.setContentType("application/json;charset=UTF-8");
            response.getWriter().write(objectMapper.writeValueAsString(e));
        } else {
            super.onAuthenticationFailure(request, response, e);
        }
    }
}

認證流程源碼級詳解

認證處理流程說明

技術分享圖片

認證結果如何在多個請求之間共享

技術分享圖片

一個請求進來的時候,先檢查context是否存有該請求的認證信息

技術分享圖片

獲取認證用戶信息

技術分享圖片

圖片驗證碼

生成圖片驗證碼

  1. 根據隨機數生成圖片
  2. 將隨機數存到Session中
  3. 在將生成的圖片寫到接口的響應中

圖片驗證碼重構

驗證碼基本參數可配置

技術分享圖片

驗證碼圖片的寬,高,字符數,失效時間可配置(註意字符數和失效時間不要在請求級配置中)。請求級配置就是在請求驗證碼時/code/image?width=100&height=30,應用級配置就是在應用的配置文件中

// 在使用這些配置時,如果請求級配置有就用請求級配置,否則就依次用應用級配置,默認配置
int width = ServletRequestUtils.getIntParameter(request.getRequest(), "width",
        securityProperties.getCode().getImage().getWidth());
int height = ServletRequestUtils.getIntParameter(request.getRequest(), "height",
        securityProperties.getCode().getImage().getHeight());

驗證碼攔截的接口可配置

默認情況下,只有在註冊,登錄的需要驗證碼的時候才攔截的,如果還有其他情景下需要則能夠在不修改依賴的情況下可配置.如何實現呢,在配置文件中添加要需要驗證碼的url,驗證碼的驗證是通過過濾器實現的,那麽在對其過濾的時候判斷當前url是否是需要攔截即可

驗證碼的生成邏輯可配置

把生成驗證碼的功能定義成接口,框架給出一個默認的實現,如果應用不定義就用這個默認實現,如果應用要定制一個,就實現這個接口就可以了.

// 框架中的默認實現不加註釋@Component進行初始化,用如下方式對其進行初始化
// 檢測上下文環境中是否有imageCodeGenerator這個bean,如果沒有就初始化框架中提供的默認實現
@Configuration
public class ValidateCodeBeanConfig {

    @Autowired
    private SecurityProperties securityProperties;

    @Bean
    @ConditionalOnMissingBean(name = "imageCodeGenerator")
    public ValidateCodeGenerator imageCodeGenerator() {
        System.out.println("init imageCodeGenerator");
        ImageCodeGenerator codeGenerator = new ImageCodeGenerator();
        codeGenerator.setSecurityProperties(securityProperties);
        return codeGenerator;
    }
}

添加記住我功能

基本原理

技術分享圖片

技術分享圖片

具體實現

@Configuration
public class BrowserSecurityConfig extends WebSecurityConfigurerAdapter {
    // 用來讀取配置
    @Autowired
    private SecurityProperties securityProperties;

    // 登錄成功後的處理
    @Autowired
    private ImoocAuthenticationSuccessHandler imoocAuthenticationSuccessHandler;

    // 登錄失敗後的處理
    @Autowired
    private ImoocAuthenticationFailHandler imoocAuthenticationFailHandler;

    @Autowired
    private DataSource dataSource;

    @Autowired
    private UserDetailsService userDetailsService;

    // 配置PasswordEncoder
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    // 用於remember me
    @Bean
    public PersistentTokenRepository persistentTokenRepository() {
        JdbcTokenRepositoryImpl tokenRepository = new JdbcTokenRepositoryImpl();
        tokenRepository.setDataSource(dataSource);
        // tokenRepository.setCreateTableOnStartup(true); // 啟動時創建表
        return tokenRepository;
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        System.out.println("BrowserSecurityConfig");
        ValidateCodeFilter validateCodeFilter = new ValidateCodeFilter();
        validateCodeFilter.setAuthenticationFailureHandler(imoocAuthenticationFailHandler);
        validateCodeFilter.setSecurityProperties(securityProperties);
        validateCodeFilter.afterPropertiesSet();

        http.addFilterBefore(validateCodeFilter, UsernamePasswordAuthenticationFilter.class)
                .formLogin() // 表單登錄
                .loginPage("/authentication/require") //  自定義登錄頁面URL
                .loginProcessingUrl("/authentication/form") // 處理登錄請求的URL
                .successHandler(imoocAuthenticationSuccessHandler) // 登錄成功後的處理
                .failureHandler(imoocAuthenticationFailHandler) // 登錄失敗後的處理
                .and()
                .rememberMe()
                .tokenRepository(persistentTokenRepository())
                .tokenValiditySeconds(securityProperties.getBrowser().getRememberMeSeconds())
                .userDetailsService(userDetailsService)
                .and()
                .authorizeRequests() // 對請求做授權
                .antMatchers("/authentication/require",
                        securityProperties.getBrowser().getLoginPage(),
                        "/code/image")
                    .permitAll() // 登錄頁面不需要認證
                .anyRequest() // 任何請求
                .authenticated() // 都需要身份認證
                .and().csrf().disable(); // 暫時將防護跨站請求偽造的功能置為不可用
    }
}

源碼解析

Spring Security之用戶名+密碼登錄