Spring Security简介
Spring Security 是一个功能强大且高度可定制的身份验证和访问控制框架。Spring Security 致力于为 Java 应用程序提供身份验证和授权的能力。
Spring Security 两大重要核心功能:用户认证(Authentication)和用户授权(Authorization)。
用户认证:验证某个用户是否为系统中的合法主体,也就是说用户能否访问该系统。用户认证一般要求用户提供用户名和密码。系统通过校验用户名和密码来完成认证过程。
用户授权:验证某个用户是否有权限执行某个操作。在一个系统中,不同用户所有的权限是不同的。比如对一个文件来说,有的用户只能进行读取,有的用户既能读取,又能修改。一般来说,系统会为不同的用户分配不同的角色,而每个角色则对应一系列的权限。
准备工作
创建Spring Boot项目
pom.xml文件(根据自己所需引入)
<dependencies>
<!-- security(安全认证) -->
<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>
<!-- mybatis-plus(数据库操作) -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.2</version>
</dependency>
<!-- redis(缓存) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- swagger(api接口文档) -->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>2.9.2</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>2.9.2</version>
</dependency>
<!-- jjwt(token生成与校验) -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
<!-- fastjson2(JSON处理) -->
<dependency>
<groupId>com.alibaba.fastjson2</groupId>
<artifactId>fastjson2</artifactId>
<version>2.0.23</version>
</dependency>
<!-- mysql(连接驱动) -->
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<!-- druid(mysql连接池) -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.2.16</version>
</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>
认证(Authentication)
登陆校验流程
原理初探
SpringSecurity 的原理其实就是一个过滤器链,内部包含了提供各种功能的过滤器。这里我们可以看看入门案例中的过滤器。
图中只展示了核心过滤器,其它的非核心过滤器并没有在图中展示。
UsernamePasswordAuthenticationFilter:负责处理我们在登陆页面填写了用户名密码后的登陆请求。入门案例的认证工作主要有它负责。
ExceptionTranslationFilter:处理过滤器链中抛出的任何AccessDeniedException和AuthenticationException 。
FilterSecurityInterceptor:负责权限校验的过滤器。
我们可以通过Debug查看当前系统中SpringSecurity过滤器链中有哪些过滤器及它们的顺序。
在控制台处点击Evaluate Expression或Alt+F8,如下图:
然后输入 run.getBean(DefaultSecurityFilterChain.class) 进行过滤,可以看到 run 容器中的 15 个过滤器:
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<SimpleGrantedAuthority> authorityList;
public CustomUserDetails() {
}
public CustomUserDetails(User user, List<String> roleList) {
this.user = user;
this.authorityList = roleList.stream()
.map(role -> new SimpleGrantedAuthority(role))
.collect(Collectors.toList());
}
@Override
public Collection<? extends GrantedAuthority> 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<String,String> 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<Claims> 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<Claims> 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);
}
}