Spring Security之用户名+密码登陆

自定义用户认证逻辑

处理用户信息获取逻辑

实现UserDetailsService接口html

@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接口的一些方法,封装了登陆时的一些信息java

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接口json

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中配置PasswordEncoderapp

// 配置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类dom

@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格式的提示信息ide

@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接口,此接口登陆成功后会被调用ui

@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(); // 暂时将防御跨站请求伪造的功能置为不可用
    }
}

源码解析