spring security

Spring-Security

2020-02-09  本文已影响0人  茶还是咖啡

Spring Security是一个功能强大且高度可定制的身份验证和访问控制框架。它是用于保护基于Spring的应用程序的实际标准。
Spring Security是一个框架,致力于为Java应用程序提供身份验证和授权。与所有Spring项目一样,Spring Security的真正强大之处在于可以轻松扩展以满足自定义要求


SpringSecurity基本原理就是使用大量的过滤器形成一个过滤器链,完成安全认证。
主要的过滤器有登陆验证过滤器,权限验证过滤器等,我们也可以增加我们自己的过滤器完成定制化开发。
请求响应时,先尝试从session中获取用户的登陆信息如果获取成功,将用户信息保存到ThreadLocal中,如果session中没有获取到用户信息依次走各个过滤器,最终将登陆成功后的用户信息存储到一个ThreadLocal中,如果执行过滤器代码时发生异常,会统一被ExceptionTranslationFilter捕获并进行处理。如果没有发生异常,最终通过橙色的FilterSecurityInterceptor拦截器后访问后台的rest服务。

SpringBoot集成SpringSecurity

  1. 引入依赖
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>

此时已经成功引入,启动项目直接访问项目地址会出现认证登录的页面



用户名为user,密码会在控制台打印


  1. 使用自己的用户名和密码
    实现 UserDetailsService接口
    该接口的loadUserByUsername方法需要返回一个UserDetail实例,
    SpringSecurity提供的User对象已经对该接口进行了实现。
    User类有两个构造方法,
public User(String username, String password,
            Collection<? extends GrantedAuthority> authorities) {
        this(username, password, true, true, true, true, authorities);
    }

第一个构造参数为传入的用户名,第二个参数为用户的密码,(这里为了方便演示,直接写死,真正开发中需要从数据库中查询)第三个参数是用户所具备的权限。

public interface UserDetails extends Serializable {
    
    Collection<? extends GrantedAuthority> getAuthorities();

    String getPassword();

    String getUsername();

    boolean isAccountNonExpired();

    boolean isAccountNonLocked();

    boolean isCredentialsNonExpired();

    boolean isEnabled();
}

该接口除了提供获取用户名密码和权限的接口外,还提供了验证用户是否过期,是否被锁定,是否冻结以及是否可用的方法。User类的第二个构造方法新增的四个参数和以上四个方法一一对应。当我们需要使用这些功能时直接使用即可。

@Slf4j
@Component
public class MyUserDetailsServiceImpl implements UserDetailsService {

    @Override
    public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
        log.info("用户名:{}", s);
        return new User(s, "123456",AuthorityUtils.commaSeparatedStringToAuthorityList("admin"));
    }
}

重新启动项目,登录会发生如下异常:



原因是由于Spring boot 2.0.3引用的security 依赖是 spring security 5.X版本,此版本需要提供一个PasswordEncorder的实例。

  1. 提供PasswordEncoder实例
    PasswordEncoder接口提供两个方法,分别是对密码进行编码和对密码进行匹配校验。
    这里暂时不使用任何编码方式,不做任何处理。再次启动项目后,可以登录成功。
@Component
public class MyPasswordEncoder implements PasswordEncoder {

    @Override
    public String encode(CharSequence charSequence) {
        return charSequence.toString();
    }

    @Override
    public boolean matches(CharSequence charSequence, String s) {
        return s.equals(charSequence.toString());
    }
}

自定义用户认证流程

自定义用户认证页面

  1. 在resources目录下的static文件夹中新增login.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>自定义用户认证逻辑</title>
</head>
<body>
    <h2>标准认证页面</h2>
    <form action="/user/login" method="post">
        <table>
            <tr>
                <td>用户名:</td>
                <td><input type="text" name="username"></td>
            </tr>
            <tr>
                <td>密码:</td>
                <td><input type="password" name="password"></td>
            </tr>
            <tr>
                <td colspan="2"><button type="submit">登录</button></td>
            </tr>
        </table>
    </form>
</body>
</html>
  1. 新增WebSecurityConfigurerAdapter配置类,覆盖configure(HttpSecurity http) throws Exception方法
@Configuration
public class BrowserSecurityConfig extends WebSecurityConfigurerAdapter {
    /**
     * 更改为任何请求都需要表单登录并且任何请求都需要授权
     *
     * @param http http
     * @throws Exception exception
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.formLogin() // 表单登录
                .loginPage("/login.html") // 增加自定义认证页面路径
                .and()
                .authorizeRequests() // 任何请求都需要认证
                .anyRequest()
                .authenticated();
    }
}

启动项目,访问项目地址会出现如下页面



原因是上面的配置的意思为所有的页面都需要认证,认证的页面为login.html而login.html也需要认证,导致页面递归跳转,最终发生以上问题。解决方式就是将认证页面放行,认证页面不需要认证。
将上面的配置修改为:

@Configuration
public class BrowserSecurityConfig extends WebSecurityConfigurerAdapter {

    /**
     * 更改为任何请求都需要表单登录并且任何请求都需要授权
     *
     * @param http http
     * @throws Exception exception
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.formLogin() // 表单登录
                .loginPage("/login.html") // 增加自定义认证页面路径
                .loginProcessingUrl("/user/login")
                .and()
                .authorizeRequests() // 任何请求都需要认证
                .antMatchers("/login.html").permitAll()
                .anyRequest()
                .authenticated()
                .and()
                .csrf()
                .disable();
    }

}

自定义认证规则

自定义登录认证页面

如果请求的是html页面,那么返回html登录页,如果是访问接口数据,返回用户需要登录json信息
此处使用到的配置类在最后会给出

  1. 创建BrowserSecurityController类
public class BrowserSecurityController {

    private HttpSessionRequestCache httpSessionRequestCache = new HttpSessionRequestCache();

    private RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();

    @Resource
    private SecurityProperties securityProperties;

    /**
     * 当需要身份认证时跳转到这里
     *
     * @param request
     * @param response
     * @return
     * @throws IOException
     */
    @GetMapping("/authentication/require")
    @ResponseStatus(HttpStatus.UNAUTHORIZED)
    public ResultVO<String> requireAuthentication(HttpServletRequest request, HttpServletResponse response) throws IOException {
        SavedRequest savedRequest = httpSessionRequestCache.getRequest(request, response);
        if (Objects.nonNull(savedRequest)) {
            String redirectUrl = savedRequest.getRedirectUrl();
            log.info("引发跳转请求URL「{}」", redirectUrl);
            if (StringUtils.endsWithIgnoreCase(redirectUrl, ".html")) {
                redirectStrategy.sendRedirect(request, response, securityProperties.getBrowser().getLoginPage());
            }
        }
        return ResultVO.buildFailure("需要用户认证");
    }

}
  1. 更换认证配置
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.formLogin() // 表单登录
                .loginPage("/authentication/require") // 增加自定义认证页面路径
                .loginProcessingUrl("/user/login")
                .and()
                .authorizeRequests() // 任何请求都需要认证
                .antMatchers("/authentication/require").permitAll()
                .anyRequest()
                .authenticated()
                .and()
                .csrf()
                .disable();
    }

自定义登录成功/失败处理

[需求]通过配置文件进行配置,如果配置的json的形式,返回json,如果配置的是redirect形式,跳转到响应的页面
如果不需要刚才的实现,只需要实现AuthenticationSuccessHandler接口,即可,但是SpringSecurity默认的处理是跳转到指定的页面,所以我们可以继承原来的逻辑,并通过判断决策使用那种方式即可。

  1. 成功处理
@Slf4j
@Component
public class CustomerAuthenticationSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler  /**AuthenticationSuccessHandler*/
{

    @Resource
    private ObjectMapper objectMapper;

    @Resource
    private SecurityProperties securityProperties;

    /**
     * 登录成功处理,这里登录成功后,将{@link Authentication}返回给前端
     *
     * @param request
     * @param response
     * @param authentication
     * @throws IOException
     * @throws ServletException
     */
    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        log.info("登录成功");
        LoginType loginType = securityProperties.getBrowser().getLoginType();
        if (LoginType.JSON.equals(loginType)) {
            response.setContentType(MediaType.APPLICATION_JSON.getType());
            response.getWriter().write(objectMapper.writeValueAsString(authentication));
        } else {
            super.onAuthenticationSuccess(request, response, authentication);
        }
    }

}

  1. 失败处理
@Slf4j
@Component
public class CustomerAuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler /**AuthenticationFailureHandler*/ {

    @Resource
    private ObjectMapper objectMapper;

    @Resource
    private SecurityProperties securityProperties;

    /**
     * 失败处理,这里如果返回的格式为json,返回异常的json串
     * @param request
     * @param response
     * @param e
     * @throws IOException
     * @throws ServletException
     */
    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException {
        log.info("登录失败");
        LoginType loginType = securityProperties.getBrowser().getLoginType();
        if(LoginType.JSON.equals(loginType)){
            response.setStatus(HttpStatus.BAD_REQUEST.value());
            response.getWriter().write(objectMapper.writeValueAsString(e));
        }else{
            super.onAuthenticationFailure(request, response, e);
        }
    }
}
  1. 通过配置类替换原有的类
@Configuration
public class BrowserSecurityConfig extends WebSecurityConfigurerAdapter {

    @Resource
    private SecurityProperties securityProperties;

    @Resource
    private CustomerAuthenticationSuccessHandler customerAuthenticationSuccessHandler;

    @Resource
    private CustomerAuthenticationFailureHandler customerAuthenticationFailureHandler;

    /**
     * 更改为任何请求都需要表单登录并且任何请求都需要授权
     *
     * @param http http
     * @throws Exception exception
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.formLogin() // 表单登录
                .loginPage("/authentication/require") // 增加自定义认证页面路径
                .loginProcessingUrl("/user/login")
                .successHandler(customerAuthenticationSuccessHandler)
                .failureHandler(customerAuthenticationFailureHandler)
                .and()
                .authorizeRequests() // 任何请求都需要认证
                .antMatchers("/login.html",
                        "/authentication/require",
                        securityProperties.getBrowser().getLoginPage()
                ).permitAll()
                .anyRequest()
                .authenticated()
                .and()
                .csrf()
                .disable();
    }

}

获取登陆成功后的用户信息

    @GetMapping("/getUserInfo")
    public ResultVO<Authentication> getUserInfo(){
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        return ResultVO.buildSuccess(authentication);
    }

配置类

  1. SecurityCoreConfig
@Configuration
@EnableConfigurationProperties(SecurityProperties.class)
public class SecurityCoreConfig {
}
  1. SecurityProperties
@Data
@ConfigurationProperties(prefix = "customer.security")
public class SecurityProperties {

    /**
     * 浏览器相关配置
     */
    private BrowserProperties browser = new BrowserProperties();

}
  1. BrowserProperties
@Data
public class BrowserProperties {
    /**
     * 用户自定义登录页面
     */
    private String loginPage = "/login.html";

    /**
     * 登陆成功响应方式{@link LoginType}
     */
    private LoginType loginType = LoginType.JSON;

}

参考文章:

上一篇 下一篇

猜你喜欢

热点阅读