目录
前言
引入依赖
原理解析
最佳实践
自定义AuthenticationFilter
自定义AuthenticationManager
自定义Provider
自定义UserDetailsService
自定义用户信息
自定义认证成功handler
配置SecurityConfig
项目验证
Spring Security作为当下最流行的认证授权框架,提供了非常全面且完善的认证授权流程,并且通过与Springboot的结合,极大的简化了开发与使用。spring官方也提供了详细的文档和丰富的应用实例。
官方文档:Spring Security
应用实例:https://github.com/thombergs/code-examples/tree/master/spring-security
但在我们的实际开发工作中,我们绝大部分都是前后端分离的项目,作为后端开发人员,我们并不关心前段页面的跳转,此外,大多数前后端业务数据都是通过JSON方式传递的,并不是Spring security 默认的form 格式。以下便是一个简单的通过JSON方式自定义登陆认证的项目简介。
org.springframework.boot
spring-boot-starter-security
spring security 提供了一个实现了DelegatingFilterProxy的Filter,使Servlet container 的生命周期和 Spring’s ApplicationContext建立联系,从而达到通过Filter对web请求进行授权认证,具体流程如下图所示:
在spring security中,所有的filter都被放在SecurityFilterChain中 按照顺序被调用处理web request.
具体的源码解析可以参考:Spring Security (一):自定义登陆_见面说Hello的博客-CSDN博客
因此,要想实现自定义登陆认证,需要自定义Filter,具体项目代码如下所示。
通过JSON方式获取用户名密码
public class CustomUsernamePasswordAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
public CustomUsernamePasswordAuthenticationFilter() {
super();
}
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
if (!request.getMethod().equals("POST") || (!request.getContentType().equalsIgnoreCase(MediaType.APPLICATION_JSON_VALUE))) {
throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
}
// 2. 判断是否是json格式请求类型
if (request.getContentType().equalsIgnoreCase(MediaType.APPLICATION_JSON_VALUE)) {
try {
CustomUser customUser = new ObjectMapper().readValue(request.getInputStream(), CustomUser.class);
String username = customUser.getUsername();
String password = customUser.getPassword();
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(username, password);
setDetails(request, authenticationToken);
return this.getAuthenticationManager().authenticate(authenticationToken);
} catch (IOException e) {
throw new AuthenticationServiceException(e.getMessage());
}
}
return super.attemptAuthentication(request, response);
}
}
public class CustomAuthenticationManager implements AuthenticationManager {
public CustomAuthenticationManager() {
super();
}
private DaoAuthenticationProvider authenticationProvider;
public void setAuthenticationProvider(DaoAuthenticationProvider authenticationProvider) {
this.authenticationProvider = authenticationProvider;
}
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
return authenticationProvider.authenticate(authentication);
}
}
public class CustomAuthenticationProvider extends DaoAuthenticationProvider {
public CustomAuthenticationProvider() {
super();
}
}
这里我为了方便仅构造了一个Map,实际应用中应从数据库获取
@Service
public class CustomUserDetailsService implements UserDetailsService {
protected static final Map map = new HashMap<>();
public static Map getMap() {
return map;
}
static {
String pwd = new BCryptPasswordEncoder().encode("userPwd");
map.put("user", new CustomUser(1L, "user", pwd));
map.put("admin", new CustomUser(2L, "admin", pwd));
}
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
CustomUser customUser = map.get(username);
if (customUser == null) {
throw new UsernameNotFoundException("username " + username + " is not found");
}
return new CustomUserDetails(customUser);
}
}
public class CustomUser implements Serializable {
private Long userId;
private String username;
private String password;
public CustomUser() {
}
public CustomUser(Long userId, String username, String password) {
this.userId = userId;
this.username = username;
this.password = password;
}
public Long getUserId() {
return userId;
}
public void setUserId(Long userId) {
this.userId = userId;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
}
public class CustomLoginSuccessHandler implements AuthenticationSuccessHandler {
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json; charset=utf-8");
response.setStatus(HttpServletResponse.SC_OK);
UserDetails customUser = (UserDetails) authentication.getPrincipal();
String username = customUser.getUsername();
response.addCookie(new Cookie("username", username));
WebResponse success = WebResponse.success(username);
response.getWriter().write(JSON.toJSONString(success));
}
}
自定义认证失败Handler
package com.study.security.config;
import com.alibaba.fastjson.JSON;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import javax.servlet.ServletException;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* @author Say Hello
* @version 1.0.0
* @Date 2023/7/27
* @Description
*/
public class CustomAuthenticationFailureHandler implements AuthenticationFailureHandler {
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json; charset=utf-8");
response.setStatus(HttpServletResponse.SC_OK);
WebResponse
package com.study.security.config;
import com.study.security.config.cookie.CustomCookieAuthenticationFilter;
import com.study.security.config.phone.CustomPhoneAuthenticationManager;
import com.study.security.config.phone.CustomPhoneAuthenticationProcessingFilter;
import com.study.security.config.phone.CustomPhoneAuthenticationProvider;
import com.study.security.service.CustomPhoneDetailsService;
import com.study.security.service.CustomUserDetailsService;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.core.annotation.Order;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import javax.annotation.Resource;
/**
* @author Say Hello
* @version 1.0.0
* @Date 2023/7/26
* @Description
*/
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Value("${spring.security.custom.url.login}")
String loginUrl;
@Resource
CustomUserDetailsService customUserDetailsService;
@Bean
public PasswordEncoder customPasswordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
@Order(1)
public SecurityFilterChain customSecurityFilterChain(HttpSecurity http) throws Exception {
http.authorizeRequests()
.anyRequest().authenticated()
.and()
.csrf().disable()
//向SecurityFilterChain中插入自定以的AuthenticationManager
.authenticationManager(customAuthenticationManager())
.authenticationManager(customPhoneAuthenticationManager())
//向SecurityFilterChain中插入自定以的AuthenticationProvider
.authenticationProvider(customAuthenticationProvider())
.authenticationProvider(customPhoneAuthenticationProvider())
.exceptionHandling().authenticationEntryPoint(new CustomAuthenticationEntryPoint())
.and()
.logout().logoutUrl(logoutUrl)
.logoutSuccessHandler(new CustomLogoutSuccessHandler());
// 将loginFilter过滤器添加到UsernamePasswordAuthenticationFilter过滤器所在的位置
http.addFilterAt(customUsernamePasswordAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
return http.build();
}
@Bean
public CustomUsernamePasswordAuthenticationFilter customUsernamePasswordAuthenticationFilter() {
CustomUsernamePasswordAuthenticationFilter customUsernamePasswordAuthenticationFilter = new CustomUsernamePasswordAuthenticationFilter();
//配置登陆认证的入口
customUsernamePasswordAuthenticationFilter.setFilterProcessesUrl(loginUrl);
//配置manager
customUsernamePasswordAuthenticationFilter.setAuthenticationManager(customAuthenticationManager());
//设置认证成功Handler
customUsernamePasswordAuthenticationFilter.setAuthenticationSuccessHandler(new CustomLoginSuccessHandler());
//设置认证失败Handler
customUsernamePasswordAuthenticationFilter.setAuthenticationFailureHandler(new CustomAuthenticationFailureHandler());
return customUsernamePasswordAuthenticationFilter;
}
@Bean
public AuthenticationManager customAuthenticationManager() {
CustomAuthenticationManager customAuthenticationManager = new CustomAuthenticationManager();
customAuthenticationManager.setAuthenticationProvider(customAuthenticationProvider());
return customAuthenticationManager;
}
@Bean
public DaoAuthenticationProvider customAuthenticationProvider() {
CustomAuthenticationProvider customAuthenticationProvider = new CustomAuthenticationProvider();
customAuthenticationProvider.setPasswordEncoder(customPasswordEncoder());
customAuthenticationProvider.setUserDetailsService(customUserDetailsService);
return customAuthenticationProvider;
}
}
未登陆访问
认证失败
认证成功