注:之前写过一些列的SpringSecurity的文章,重新写一遍是为了把某些不必要的步骤省去,留下精简的,因为工作中有一些不需要。

在java的权限框架里,shiro和SpringSecurity是用的最多的。随着springboot的流行,SpringSecurity也越来越火了,因为springboot默认支持SpringSecurity。所以很有必要把SpringSecurity也搞明白。shiro更加轻量级,SpringSecurity的功能更加丰富。

软件环境:

  开发工具:Idea

  JDK:1.8   

  maven:3.3.9

     * SpringBoot版本:1.5.18  (说明:springboot版本没有用最新版的2.0x,因为2.0x和1.5 差别比较大吧,2.x好似是基于spring5的,SpringSecurity的更新速度好似跟不上springboot,所以这里如果用springboot 2.x会有问题,保险起见这里就不求新了)

SpringSecurity核心功能:认证、授权、攻击防护(防止伪造身份)

话不多说,下边开始打代码。

一、项目搭建

1.1  热身 ,SpringSecurity默认的Httpbasic鉴权

1.1.1,新建项目

填好项目信息,next

选启动器:SpringSecurity、Web

设置项目目录。finish

建好后的项目结构:security包放有关SpringSecurity的代码

maven依赖:

<dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

引入了SpringSecurity的starter后,会自动引入SpringSecurity相关的相关依赖:

 1.1.2 访问我们的服务

 新建一个Controller:

@RestController
public class UserController {

    @RequestMapping("/hello")
    public String hello(){
        return "Hello ! ";
    }
}

启动项目,访问localhost:8080/hello,默认就会出来这个登录框,只要引入了SpringSecurity,springboot就默认会启用http basic认证,所有的服务都会被保护起来, ,。

其中的默认用户名是user,默认的密码在启动项目的时候会在控制台打印。

输入用户名密码,就能访问:

 

如果输入的用户名或密码错误,Springboot会引导你到一个空白页,这个也是可以配置的,后边再说怎么配置:

你还可以关闭这个默认的鉴权,只需在application.properties 里配置:  security.basic.enabled = false ,这个值默认是true。

二、表单登录

如果想去掉那个比较丑的basic登录框,只需要一个配置类即可。新建配置类 BrowserSecurityConfig,继承  WebSecurityConfigurerAdapter

@EnableWebSecurity
public class BrowserSecurityConfig extends WebSecurityConfigurerAdapter {
    
}

(为什么这么写?因为SpringSecurity官方文档这么说的https://docs.spring.io/spring-security/site/docs/4.2.10.RELEASE/reference/htmlsingle/#samples):

    5.1 Hello Web Security Java配置

    第一步是创建Spring Security Java配置。配置创建一个Servlet过滤器,称为springSecurityFilterChain负责应用程序内所有安全性(保护应用程序URL,验证提交的用户名和密码,重定向到登录表单等)。您可以在下面找到Spring Security Java配置的最基本示例:

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.context.annotation.*;
import org.springframework.security.config.annotation.authentication.builders.*;
import org.springframework.security.config.annotation.web.configuration.*;

@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Bean
    public UserDetailsService userDetailsService() throws Exception {
        InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
        manager.createUser(User.withUsername("user").password("password").roles("USER").build());
        return manager;
    }
}
然后启动application,访问http://localhost:8080/hello,可以看到已经出来了一个登录页面,输入用户名user,以及控制台打印的密码:

登录后 访问的是登录之前的要访问的hello服务:

官网说明:

到目前为止,我们的WebSecurityConfig仅包含有关如何验证用户身份的信息。Spring Security如何知道我们要求所有用户进行身份验证?Spring Security如何知道我们想要支持基于表单的身份验证?原因是WebSecurityConfigurerAdapterconfigure(HttpSecurity http)方法中提供了一个默认配置,如下所示:

protected void configure(HttpSecurity http) throws Exception {
    http
        .authorizeRequests()
            .anyRequest().authenticated()
            .and()
        .formLogin()
            .and()
        .httpBasic();
}

三、自定义用户认证逻辑

3.1 ,处理用户信息获取 、用户校验  、密码的加密解密

新建一个类,MyUserDetailService (这个类必须是Spring里的一个Bean,所以加上@Component注解)实现 UserDetailsService 接口,UserDetailsService 接口只有一个方法:通过用户名查询用户信息,返回UserDetail

UserDetailsService.java 
public interface UserDetailsService {
    UserDetails loadUserByUsername(String var1) throws UsernameNotFoundException;
}
UserDetails.java : 提供了几个方法,账户是否启用、账户是否过期、密码是否过期、账户是否锁定、权限集合信息
public interface UserDetails extends Serializable {
    Collection<? extends GrantedAuthority> getAuthorities();

    String getPassword();

    String getUsername();

    boolean isAccountNonExpired();

    boolean isAccountNonLocked();

    boolean isCredentialsNonExpired();

    boolean isEnabled();
}

 配置一个密码的加密解密器类:PasswordEncoder,我们用其一个实现类 BCryptPasswordEncoder,只有两个方法,一个是加密密码,一个是匹配方法。如果已有系统已经有了自己的加密算法,这里可以换成自己的加解密逻辑。

  @Bean
    public PasswordEncoder passwordencoder(){
        //BCryptPasswordEncoder implements PasswordEncoder
        return new BCryptPasswordEncoder();
    }

这个接口的实现类会给加密的密码随机加盐,所以一样的密码每次加密出来是不一样的,更安全。如123456加密2次:

加密后密码:  $2a$10$BChH.C4.X8MYuI1mHFoOkefWhOsad7SvhZedHFt1OG4vjSu.z9weC

加密后密码:  $2a$10$YUbz.miE5C0aAcuU1FnHSu/U.Qm/BujTNw6X7S5i4/6AhjyDc6suK

 此时我们的逻辑是,只要密码输入123456,就能登录成功。启动项目试验:访问/hello ,会自动跳转/login ,随便输入用户名aaaaaa,输入密码123456 ,成功访问Hello!

    |     

 3.2 自定义登录页面

用SpringSecurity提供的默认的登录页肯定是不行的,所以需要自定义登录页(当然这里还可以配置成一个Controller方法,然后跳转到登录页。或者从配置文件里读取),在BrowserSecurityConfig类里配置:

@Override
    protected void configure(HttpSecurity http) throws Exception {
        //http.httpBasic() //这个就是默认的弹框认证
        http.formLogin() //表单认证
                .loginPage("/login.html")
                .loginProcessingUrl("/authentication/form")
                .and()
                .authorizeRequests() //下边的都是授权的配置
                .antMatchers("/login.html").permitAll() //放过登录页不过滤
                .anyRequest()        //任何请求
                .authenticated();    //都需要身份认证
    }
.loginPage("/login.html") 配置登录页面
.loginProcessingUrl("/authentication/form") 配置处理登录表单的action,这个值默认在UsernamePasswordAuthenticationFilter 类里,

   注意一定要放过登录页不过滤,否则一直不能跳转到login.html 

此时访问 /hello ,跳转到自定义的登录页:

随便输入用户名,密码输入123456 ,登录,出现

SpringSecurity默认提供了CSRF(跨站请求伪造)防护,是用CSRF token来完成防护的。暂时关闭CSRF防护,在BrowserSecurityConfig里设置:

.csrf().disable();
再次登录,可以成功访问到 /hello服务。
 至此自定义登录页面完成吗,下一步自定义登录成功处理。
也可以把登录页做成在配置文件application.properties里配置,然后在代码里读取配置,这样更灵活、更通用。
这里有springboot读取配置文件的用法:https://www.cnblogs.com/lihaoyang/p/10223339.html

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

现在的流程是,访问一个需要登录的服务,如果没有登录,跳转登录页,等你登录后,立马就访问登录之前的服务。如果你想在登录成功后做一些处理,比如签到,送积分等等。那就需要自定义登录成功的处理。如果没有这种需求,这一步骤可以略过。

springsecurity提供了一个接口,AuthenticationSuccessHandler,用来处理登录成功后的逻辑:

 

/**
 * Strategy used to handle a successful user authentication.
 * <p>
 * Implementations can do whatever they want but typical behaviour would be to control the
 * navigation to the subsequent destination (using a redirect or a forward). For example,
 * after a user has logged in by submitting a login form, the application needs to decide
 * where they should be redirected to afterwards (see
 * {@link AbstractAuthenticationProcessingFilter} and subclasses). Other logic may also be
 * included if required.
 *
 * @author Luke Taylor
 * @since 3.0
 */
public interface AuthenticationSuccessHandler {

    /**
     * Called when a user has been successfully authenticated.
     *
     * @param request the request which caused the successful authentication
     * @param response the response
     * @param authentication the <tt>Authentication</tt> object which was created during
     * the authentication process.
     */
    void onAuthenticationSuccess(HttpServletRequest request,
            HttpServletResponse response, Authentication authentication)
            throws IOException, ServletException;

}

 

1,定义一个自己的成功处理器,实现这个Handler:

package com.lhy.browser.security.authentication;

import com.fasterxml.jackson.databind.ObjectMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.stereotype.Component;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * 认证成功处理器
 */
@Component("myAuthenticationSuccessHandler")
public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler {

    private Logger logger = LoggerFactory.getLogger(getClass());

    //springmvc启动会自动注册一个ObjectMapper
    @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));

    }
}

2、添加到配置类,让spring security执行自定义的处理器

 在 BrowserSecurityConfig  配置类里注入 AuthenticationSuccessHandler

@EnableWebSecurity
public class BrowserSecurityConfig extends WebSecurityConfigurerAdapter {


    //自定义的认证成功的处理器
    @Autowired
    private AuthenticationSuccessHandler myAuthenticationSuccessHandler;
    //自定义的认证失败的处理器
    @Autowired
    private AuthenticationFailureHandler myAuthenticationFailureHandler;


    @Autowired
    private SecurityProperties securityProperties;


    @Override
    protected void configure(HttpSecurity http) throws Exception {
        //http.httpBasic() //这个就是默认的弹框认证
        http.formLogin() //表单认证
                .loginPage(securityProperties.getBrowser().getLoginPage())//登录页
                .loginProcessingUrl("/authentication/form") //登录提交action
                .successHandler(myAuthenticationSuccessHandler) //自定义的认证成功处理器
                .failureHandler(myAuthenticationFailureHandler)//自定义的认证失败的处理器
                .and()
                .authorizeRequests() //下边的都是授权的配置
                .antMatchers(securityProperties.getBrowser().getLoginPage()).permitAll() //放过登录页不过滤
                .anyRequest()        //任何请求
                .authenticated()    //都需要身份认证
                .and()
                .csrf().disable();
    }


}

登录失败处理器和登录成功类似,自定义失败处理类,实现  AuthenticationFailureHandler  接口即可:

/**
 * 认证失败处理器
 */
@Component("/myAuthenticationFailureHandler")
public class MyAuthenticationFailureHandler  implements AuthenticationFailureHandler {

    private Logger logger = LoggerFactory.getLogger(getClass());

    //springmvc启动会自动注册一个ObjectMapper
    @Autowired
    private ObjectMapper objectMapper;


    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
        logger.info("登录失败");

        //把authentication返回给响应
        //状态码500,服务器内部错误
        response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
        response.setContentType("application/json;charset=UTF-8");
        response.getWriter().write(objectMapper.writeValueAsString(exception.getMessage()));
    }
}

这样就可以在登录成功和失败后进入自己的处理逻辑了。

 

欢迎关注个人公众号一起交流学习: