Spring Boot + Spring Security基础入门教程

Spring Security简介

Spring Security 是一个功能强大且高度可定制的身份验证和访问控制框架。Spring Security 致力于为 Java 应用程序提供身份验证和授权的能力。

Spring Security 两大重要核心功能:用户认证(Authentication)用户授权(Authorization)

用户认证:验证某个用户是否为系统中的合法主体,也就是说用户能否访问该系统。用户认证一般要求用户提供用户名和密码。系统通过校验用户名和密码来完成认证过程。

用户授权:验证某个用户是否有权限执行某个操作。在一个系统中,不同用户所有的权限是不同的。比如对一个文件来说,有的用户只能进行读取,有的用户既能读取,又能修改。一般来说,系统会为不同的用户分配不同的角色,而每个角色则对应一系列的权限。

准备工作

创建Spring Boot项目

pom.xml文件(根据自己所需引入)

    
        
        
            org.springframework.boot
            spring-boot-starter-security
        
        
            org.springframework.boot
            spring-boot-starter-web
        
        
        
            com.baomidou
            mybatis-plus-boot-starter
            3.5.2
        
        
        
            org.springframework.boot
            spring-boot-starter-data-redis
        

        
        
            io.springfox
            springfox-swagger2
            2.9.2
        
        
            io.springfox
            springfox-swagger-ui
            2.9.2
        

        
        
            io.jsonwebtoken
            jjwt
            0.9.1
        
        
        
            com.alibaba.fastjson2
            fastjson2
            2.0.23
        

        
        
            com.mysql
            mysql-connector-j
            runtime
        
        
        
            com.alibaba
            druid-spring-boot-starter
            1.2.16
        
        
            org.springframework.boot
            spring-boot-starter-test
            test
        
        
            org.springframework.security
            spring-security-test
            test
        
    

认证(Authentication)

登陆校验流程

Spring Boot + Spring Security基础入门教程_第1张图片

原理初探

SpringSecurity 的原理其实就是一个过滤器链,内部包含了提供各种功能的过滤器。这里我们可以看看入门案例中的过滤器。

Spring Boot + Spring Security基础入门教程_第2张图片

图中只展示了核心过滤器,其它的非核心过滤器并没有在图中展示。

UsernamePasswordAuthenticationFilter:负责处理我们在登陆页面填写了用户名密码后的登陆请求。入门案例的认证工作主要有它负责。

ExceptionTranslationFilter:处理过滤器链中抛出的任何AccessDeniedException和AuthenticationException 。

FilterSecurityInterceptor:负责权限校验的过滤器。

我们可以通过Debug查看当前系统中SpringSecurity过滤器链中有哪些过滤器及它们的顺序。

在控制台处点击Evaluate Expression或Alt+F8,如下图:Spring Boot + Spring Security基础入门教程_第3张图片

 然后输入 run.getBean(DefaultSecurityFilterChain.class) 进行过滤,可以看到 run 容器中的 15 个过滤器:

Spring Boot + Spring Security基础入门教程_第4张图片

Spring Security配置类

import com.zm.springsecurity.common.filter.CustomAuthenticationFilter;
import com.zm.springsecurity.common.security.CustomAuthenticationFailureHandler;
import com.zm.springsecurity.common.security.CustomAuthenticationSuccessHandler;
import com.zm.springsecurity.common.security.CustomLogoutSuccessHandler;
import com.zm.springsecurity.service.impl.CustomUserDetailsServiceImpl;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true) // 开启方法级安全
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    private static final String URL_WHITELIST[] ={
            "/v2/api-docs", "/swagger-resources/configuration/ui",
            "/swagger-resources", "/swagger-resources/configuration/security",
            "/swagger-ui.html", "/webjars/**", // swagger不需要授权即可访问的路径

            "/login",
            "/logout",
            "/my/login",
            "/my/logout",
            "/captcha",
            "/password",
            "/image/**",
            "/test/**"
    };

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

    @Bean
    protected CustomAuthenticationFilter customAuthenticationFilter() throws Exception {
        CustomAuthenticationFilter authenticationFilter = new CustomAuthenticationFilter();
        authenticationFilter.setFilterProcessesUrl("/my/login");
        authenticationFilter.setUsernameParameter("username");
        authenticationFilter.setPasswordParameter("password");
        authenticationFilter.setAuthenticationManager(super.authenticationManager());
        authenticationFilter.setAuthenticationSuccessHandler(new CustomAuthenticationSuccessHandler());
        authenticationFilter.setAuthenticationFailureHandler(new CustomAuthenticationFailureHandler());
        return authenticationFilter;
    }

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

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.cors().and().csrf().disable() // 开启跨域请求和关闭csrf攻击
                .userDetailsService(new CustomUserDetailsServiceImpl())
//                .formLogin().loginPage("/login_page")
//                .loginProcessingUrl("/my/login")
//                .usernameParameter("username").passwordParameter("password").permitAll()
//                .successHandler(new CustomAuthenticationSuccessHandler()) // 认证成功处理器
//                .failureHandler(new CustomAuthenticationFailureHandler()) // 认证失败处理器
//                .and()
                .logout()
                .logoutUrl("/my/logout")
                .logoutSuccessHandler(new CustomLogoutSuccessHandler()) // 退出登录成功处理器
                .and()
                .sessionManagement()
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS) // session禁用配置(无状态)
                .and()
                .authorizeRequests()  // 验证请求拦截规则
                .antMatchers(URL_WHITELIST).permitAll() // 配置访问认证白名单
                .antMatchers("/admin/**").hasRole("admin") // 要具有某种权限
                .antMatchers("/user/**").hasAnyRole("admin", "user") // 要具有某种权限中的一种
                .anyRequest().authenticated();

        http.addFilterAt(this.customAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
    }
}

使用数据库进行认证

注:本文采用 MyBatis-Plus 作为持久层框架,与 MyBatis-Plus 相关内容自行编写。

实现UserDetails接口

import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import java.util.Collection;
import java.util.List;
import java.util.stream.Collectors;

public class CustomUserDetails implements UserDetails {
    private User user;
    private List authorityList;

    public CustomUserDetails() {
    }

    public CustomUserDetails(User user, List roleList) {
        this.user = user;
        this.authorityList = roleList.stream()
                .map(role -> new SimpleGrantedAuthority(role))
                .collect(Collectors.toList());
    }

    @Override
    public Collection getAuthorities() {
        return this.authorityList;
    }

    @Override
    public String getPassword() {
        return user.getPassword();
    }

    @Override
    public String getUsername() {
        return user.getPassword();
    }

    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return user.getStatus();
    }
}

自定义UsernamePasswordAuthenticationFilter过滤器

若处理请求为表单类型的数据,则此步忽略并删除 Security 配置类中 CustomAuthenticationFilter 相关的内容。UsernamePasswordAuthenticationFilter 为认证过滤器,默认只能处理表单提交的数据,如需处理 JSON 数据,则需要重写 UsernamePasswordAuthenticationFilter 的 attemptAuthentication() 方法。

import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.http.MediaType;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.InputStream;
import java.util.Map;

/**
 * 登录认证过滤器,处理认证的请求体为 JSON 的数据
 */
public class CustomAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
        String contentType = request.getContentType();
        logger.info("contentType = " + contentType);
        if (!Objects.isNull(contentType) && (contentType.equals(MediaType.APPLICATION_JSON_VALUE) || contentType.equals(MediaType.APPLICATION_JSON_UTF8_VALUE))) {
            UsernamePasswordAuthenticationToken authRequest = null;
            try (InputStream inputStream = request.getInputStream()) {
                ObjectMapper mapper = new ObjectMapper(); // JSON数据映射器
                Map params = mapper.readValue(inputStream, Map.class);
                authRequest = new UsernamePasswordAuthenticationToken(params.get("username"), params.get("password"));
            } catch (IOException e) {
                e.printStackTrace();
                authRequest = new UsernamePasswordAuthenticationToken("", "");
            } finally {
                setDetails(request, authRequest);
                return this.getAuthenticationManager().authenticate(authRequest);
            }
        }
        else {
            return super.attemptAuthentication(request, response);
        }
    }
}

自定义处理器

JWT工具类

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jws;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.springframework.util.StringUtils;

import java.util.Date;

public class JWTUtils {
    private static final String tokenSignKey = "zm_sign_key"; // 私钥(盐),太短会报异常:secret key byte array cannot be null or empty.
    private static final Integer tokenExpiration = 60 * 60 * 24 * 14; // 14天

    public static String createToken(String username){
        String token = Jwts.builder()
                .setSubject("AUTH-USER")
                .setExpiration(new Date(System.currentTimeMillis() + tokenExpiration))
                .claim("username", username)
                .signWith(SignatureAlgorithm.HS512, tokenSignKey)
                .compact();
        return token;
    }

    public static String createToken(Long userId, String username){
        String token = Jwts.builder()
                .setSubject("AUTH-USER")
                .setExpiration(new Date(System.currentTimeMillis() + tokenExpiration))
                .claim("userId", userId)
                .claim("username", username)
                .signWith(SignatureAlgorithm.HS512, tokenSignKey)
                .compact();
        return token;
    }

    public static Long getUserId(String token) {
        try {
            if (!StringUtils.hasLength(token)) {
                return null;
            }

            Jws claimsJws = Jwts.parser().setSigningKey(tokenSignKey).parseClaimsJws(token);
            Claims claims = claimsJws.getBody();
            return claims.get("userId", Long.class);
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }

    public static String getUsername(String token) {
        try {
            if (!StringUtils.hasLength(token)) {
                return "";
            }

            Jws claimsJws = Jwts.parser().setSigningKey(tokenSignKey).parseClaimsJws(token);
            Claims claims = claimsJws.getBody();
            return claims.get("username", String.class);
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }
}

响应JSON数据信息

import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;

public class ResponseUtils {
    public static void response(HttpServletResponse response, String data) throws IOException {
        response.setContentType("text/html;charset=utf-8");
        PrintWriter responseWriter = response.getWriter();
        responseWriter.write(data);
        responseWriter.flush();
        responseWriter.close();
    }
}

自定义认证成功处理器

import com.alibaba.fastjson2.JSON;
import com.zm.springsecurity.utils.JWTUtils;
import com.zm.springsecurity.utils.ResponseUtils;
import com.zm.springsecurity.utils.ResultUtils;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;

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

public class CustomAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        String customUserDetails = authentication.getPrincipal().toString();  // 继承UserDetails的对象
        String token = JWTUtils.createToken(username);
        String jsonString = JSON.toJSONString(ResultUtils.ok("登录成功", token));
        ResponseUtils.response(response, jsonString);
    }
}

为什么 authentication.getPrincipal() 的结果是继承 UserDetails 的对象:ProviderManager 中的 this.copyDetails(authentication, result); 语句。

    private void copyDetails(Authentication source, Authentication dest) {
        if (dest instanceof AbstractAuthenticationToken && dest.getDetails() == null) {
            AbstractAuthenticationToken token = (AbstractAuthenticationToken)dest;
            token.setDetails(source.getDetails());
        }

    }

自定义认证失败处理器

import com.alibaba.fastjson2.JSON;
import com.zm.springsecurity.utils.ResponseUtils;
import com.zm.springsecurity.utils.ResultUtils;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;

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

public class CustomAuthenticationFailureHandler implements AuthenticationFailureHandler {
    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
        String message = exception.getMessage();
        if(exception instanceof BadCredentialsException){
            message = "用户名或密码错误!";
        }
        String jsonString = JSON.toJSONString(ResultUtils.fail(message));
        ResponseUtils.response(response, jsonString);
    }
}

自定义注销成功处理器

import com.alibaba.fastjson2.JSON;
import com.zm.springsecurity.utils.ResponseUtils;
import com.zm.springsecurity.utils.ResultUtils;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;

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

public class CustomLogoutSuccessHandler implements LogoutSuccessHandler {
    @Override
    public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        String jsonString = JSON.toJSONString(ResultUtils.ok("退出登录成功!"));
        ResponseUtils.response(response, jsonString);
    }
}

授权(Authorization

未完待续... 

你可能感兴趣的:(spring,java,spring,boot,后端)