springboot 2.7.X 整合 spring security
- spring security
- 什么是spring security
- 多的就不赘述了,直接上干货
- 引入spring security依赖
- 下面我们先简单看下相关源码,只提主要部分
- 配置 WebSecurityConfig
- UsernamePasswordAuthenticationFliter
- UsernamePasswordAuthenticationToken
- ProviderManager
- AbstractUserDetailsAuthenticationProvider
- DaoAuthenticationProvider
- UserDetailsService
- 简单的自定义实现
- 引入相关依赖
- 配置数据库
- 创建 TestController类
- 创建 LoginUser类 实现 UserDetails
- 创建 MyUserDetailsService 类 继承 UserDetailsService 方便后面扩展
- 创建 MyUserDetailsServiceImpl 类 实现 MyUserDetailsService
- 创建 MyMobileAuthenticationFilter类
- 创建 MobileAuthenticationToken 类
- 创建 MyMobileAuthenticationProvider类
- 创建 MyAuthenticationSuccessHandler类
- 创建 MyAuthenticationFailureHandler类
- 创建 MyMobileSecurityConfig类
- 创建 RequestTokenFilter 类 校验token
- 创建 MyAuthenticationEntryPointHandler 类
- 最后完善第一步的 WebSecurityConfig 配置
- 至此,简单的自定义登录实现就完成了,我们来测试一下
spring security
踩完坑后做个记录,以免忘记,好记性不如烂笔头。
本文是基于springboot 2.7.16 整合 spring security 5.7.11
该教程主要针对前后端分离项目,其他的就不多讲了。
什么是spring security
spring security是一个提供认证、授权,能防止常见攻击的安全框架。它支持servlet application 和 reactive application。 具体的可以参考 官方文档,本文主要讲 servlet application
本质是通过一个filter链实现
处理身份认证主要的过滤器
以下是security的过滤器排序列表
多的就不赘述了,直接上干货
引入spring security依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
引入依赖后访问 服务器地址+端口号出现如下,表示你的接口已被 security 保护
spring security默认的用户名是user,密码在启动服务器的时候会生成
密码:
也可以在yml配置中自己配置
spring:
security:
user:
name: admin
password: admin
下面我们先简单看下相关源码,只提主要部分
配置 WebSecurityConfig
以前版本的 WebSecurityConfigurerAdapter已被弃用,不再去继承,现在需要以 bean 的方式注入
@EnableWebSecurity
public class WebSecurityConfig {
/**
* 安全过滤器,配置 URL 的安全配置
* <p>
* anyRequest | 匹配所有请求路径
* access | SpringEl表达式结果为true时可以访问
* anonymous | 匿名可以访问
* denyAll | 用户不能访问
* fullyAuthenticated | 用户完全认证可以访问(非remember-me下自动登录)
* hasAnyAuthority | 如果有参数,参数表示权限,则其中任何一个权限可以访问
* hasAnyRole | 如果有参数,参数表示角色,则其中任何一个角色可以访问
* hasAuthority | 如果有参数,参数表示权限,则其权限可以访问
* hasIpAddress | 如果有参数,参数表示IP地址,如果用户IP和参数匹配,则可以访问
* hasRole | 如果有参数,参数表示角色,则其角色可以访问
* permitAll | 用户可以任意访问
* rememberMe | 允许通过remember-me登录的用户访问
* authenticated | 用户登录后可访问
*/
@Bean
SecurityFilterChain defaultSecurityFilterChain(HttpSecurity httpSecurity) throws Exception {
httpSecurity
//开启跨域
// .cors().and()
//禁用CSRF,因为不使用 session,前后端分离项目不需要
.csrf().disable()
//禁用session,前后端分离项目基于token不需要
.sessionManagement(AbstractHttpConfigurer::disable)
.authorizeRequests(
authorizeRequests -> authorizeRequests.antMatchers("/toLogin").permitAll()
.anyRequest().authenticated()
);
return httpSecurity.build();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
UsernamePasswordAuthenticationFliter
上面说到security的本质是由多个过滤器组成一个链实现的,接下来我们跟踪一下源码,
只提主要部分,从开始的图中我们看到 SecurityFilterChain 中开始会进入到一个抽象的过滤器中 AbstractAuthenticationProcessingFilter , 我们使用用户名密码登录,其中用到的过滤器是UsernamePasswordAuthenticationFliter,它继承了AbstractAuthenticationProcessingFilter ,主要用到的方法attemptAuthentication
/**
* 身份验证
*/
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
throws AuthenticationException {
if (this.postOnly && !request.getMethod().equals("POST")) {
throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
}
String username = obtainUsername(request);
username = (username != null) ? username.trim() : "";
String password = obtainPassword(request);
password = (password != null) ? password : "";
UsernamePasswordAuthenticationToken authRequest = UsernamePasswordAuthenticationToken.unauthenticated(username,
password);
// Allow subclasses to set the "details" property
setDetails(request, authRequest);
//进入 AuthenticationManager 相关处理
return this.getAuthenticationManager().authenticate(authRequest);
}
UsernamePasswordAuthenticationToken
一个身份认证的实现
public class UsernamePasswordAuthenticationToken extends AbstractAuthenticationToken {
private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;
private final Object principal;
private Object credentials;
/**
* This constructor can be safely used by any code that wishes to create a
* <code>UsernamePasswordAuthenticationToken</code>, as the {@link #isAuthenticated()}
* will return <code>false</code>.
*
*/
public UsernamePasswordAuthenticationToken(Object principal, Object credentials) {
super(null);
this.principal = principal;
this.credentials = credentials;
setAuthenticated(false);
}
/**
* This constructor should only be used by <code>AuthenticationManager</code> or
* <code>AuthenticationProvider</code> implementations that are satisfied with
* producing a trusted (i.e. {@link #isAuthenticated()} = <code>true</code>)
* authentication token.
* @param principal
* @param credentials
* @param authorities
*/
public UsernamePasswordAuthenticationToken(Object principal, Object credentials,
Collection<? extends GrantedAuthority> authorities) {
super(authorities);
this.principal = principal;
this.credentials = credentials;
super.setAuthenticated(true); // must use super, as we override
}
/**
* This factory method can be safely used by any code that wishes to create a
* unauthenticated <code>UsernamePasswordAuthenticationToken</code>.
* @param principal
* @param credentials
* @return UsernamePasswordAuthenticationToken with false isAuthenticated() result
*
* @since 5.7
*/
public static UsernamePasswordAuthenticationToken unauthenticated(Object principal, Object credentials) {
return new UsernamePasswordAuthenticationToken(principal, credentials);
}
/**
* This factory method can be safely used by any code that wishes to create a
* authenticated <code>UsernamePasswordAuthenticationToken</code>.
* @param principal
* @param credentials
* @return UsernamePasswordAuthenticationToken with true isAuthenticated() result
*
* @since 5.7
*/
public static UsernamePasswordAuthenticationToken authenticated(Object principal, Object credentials,
Collection<? extends GrantedAuthority> authorities) {
return new UsernamePasswordAuthenticationToken(principal, credentials, authorities);
}
@Override
public Object getCredentials() {
return this.credentials;
}
@Override
public Object getPrincipal() {
return this.principal;
}
@Override
public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException {
Assert.isTrue(!isAuthenticated,
"Cannot set this token to trusted - use constructor which takes a GrantedAuthority list instead");
super.setAuthenticated(false);
}
@Override
public void eraseCredentials() {
super.eraseCredentials();
this.credentials = null;
}
}
ProviderManager
进入到 ProviderManager 的authenticate方法, ProviderManager 实现了AuthenticationManager接口
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
Class<? extends Authentication> toTest = authentication.getClass();
AuthenticationException lastException = null;
AuthenticationException parentException = null;
Authentication result = null;
Authentication parentResult = null;
int currentPosition = 0;
int size = this.providers.size();
for (AuthenticationProvider provider : getProviders()) {
//判断 AuthenticationProvider 是否支持所使用的类
if (!provider.supports(toTest)) {
continue;
}
if (logger.isTraceEnabled()) {
logger.trace(LogMessage.format("Authenticating request with %s (%d/%d)",
provider.getClass().getSimpleName(), ++currentPosition, size));
}
try {
//进入provider的认证方法
result = provider.authenticate(authentication);
if (result != null) {
copyDetails(authentication, result);
break;
}
}
catch (AccountStatusException | InternalAuthenticationServiceException ex) {
prepareException(ex, authentication);
// SEC-546: Avoid polling additional providers if auth failure is due to
// invalid account status
throw ex;
}
catch (AuthenticationException ex) {
lastException = ex;
}
}
AbstractUserDetailsAuthenticationProvider
上面提到的authenticate方法
public abstract class AbstractUserDetailsAuthenticationProvider
implements AuthenticationProvider, InitializingBean, MessageSourceAware {
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication,
() -> this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.onlySupports",
"Only UsernamePasswordAuthenticationToken is supported"));
String username = determineUsername(authentication);
boolean cacheWasUsed = true;
//根据用户名从缓存中获取用户信息
UserDetails user = this.userCache.getUserFromCache(username);
if (user == null) {
cacheWasUsed = false;
try {
//调用实现类DaoAuthenticationProvider的retrieveUser,这里会得到保存在内存中的用户信息,如密码,账号状态等
user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication);
}
catch (UsernameNotFoundException ex) {
this.logger.debug("Failed to find user '" + username + "'");
if (!this.hideUserNotFoundExceptions) {
throw ex;
}
throw new BadCredentialsException(this.messages
.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
}
Assert.notNull(user, "retrieveUser returned null - a violation of the interface contract");
}
try {
//判断用户状态
this.preAuthenticationChecks.check(user);
//密码比较
additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication);
}
catch (AuthenticationException ex) {
if (!cacheWasUsed) {
throw ex;
}
// There was a problem, so try again after checking
// we're using latest data (i.e. not from the cache)
cacheWasUsed = false;
user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication);
this.preAuthenticationChecks.check(user);
additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication);
}
this.postAuthenticationChecks.check(user);
if (!cacheWasUsed) {
this.userCache.putUserInCache(user);
}
Object principalToReturn = user;
if (this.forcePrincipalAsString) {
principalToReturn = user.getUsername();
}
//创建一个成功的Authentication对象
return createSuccessAuthentication(principalToReturn, authentication, user);
}
}
DaoAuthenticationProvider
DaoAuthenticationProvider继承了AbstractUserDetailsAuthenticationProvider,
获取用户信息的方法 retrieveUser
public class DaoAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider {
@Override
protected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication)
throws AuthenticationException {
prepareTimingAttackProtection();
try {
//查询用户信息
UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
if (loadedUser == null) {
throw new InternalAuthenticationServiceException(
"UserDetailsService returned null, which is an interface contract violation");
}
return loadedUser;
}
catch (UsernameNotFoundException ex) {
mitigateAgainstTimingAttack(authentication);
throw ex;
}
catch (InternalAuthenticationServiceException ex) {
throw ex;
}
catch (Exception ex) {
throw new InternalAuthenticationServiceException(ex.getMessage(), ex);
}
}
}
UserDetailsService
要自己从数据库查询用户信息需要自己实现 UserDetailsService 接口
public interface UserDetailsService {
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}
这里我们主要从内存中查找,默认机制使用 InMemoryUserDetailsManager ,UserDetailsManager继承了 UserDetailsService
public class InMemoryUserDetailsManager implements UserDetailsManager, UserDetailsPasswordService {
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//(从缓存中获取用户信息
UserDetails user = this.users.get(username.toLowerCase());
if (user == null) {
throw new UsernameNotFoundException(username);
}
//(2)返回一个UserDetails对象
return new User(user.getUsername(), user.getPassword(), user.isEnabled(), user.isAccountNonExpired(),
user.isCredentialsNonExpired(), user.isAccountNonLocked(), user.getAuthorities());
}
}
使用的用户信息对象需要实现 UserDetails 接口
public class User implements UserDetails, CredentialsContainer {
private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;
private static final Log logger = LogFactory.getLog(User.class);
private String password;
private final String username;
private final Set<GrantedAuthority> authorities;
private final boolean accountNonExpired;
private final boolean accountNonLocked;
private final boolean credentialsNonExpired;
private final boolean enabled;
public User(String username, String password, Collection<? extends GrantedAuthority> authorities) {
this(username, password, true, true, true, true, authorities);
}
public User(String username, String password, boolean enabled, boolean accountNonExpired,
boolean credentialsNonExpired, boolean accountNonLocked,
Collection<? extends GrantedAuthority> authorities) {
Assert.isTrue(username != null && !"".equals(username) && password != null,
"Cannot pass null or empty values to constructor");
this.username = username;
this.password = password;
this.enabled = enabled;
this.accountNonExpired = accountNonExpired;
this.credentialsNonExpired = credentialsNonExpired;
this.accountNonLocked = accountNonLocked;
this.authorities = Collections.unmodifiableSet(sortAuthorities(authorities));
}
@Override
//权限
public Collection<GrantedAuthority> getAuthorities() {
return this.authorities;
}
}
认证成功后会把用户信息放到 SecurityContextHolder 中,这里就不说了自行查看SecurityContextPersistenceFilter 源码
简单的自定义实现
引入相关依赖
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.3.1</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.33</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.83</version>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.16</version>
</dependency>
</dependencies>
配置数据库
server:
port: 8080
spring:
application:
name: demo-security
profiles:
active: dev
main:
allow-bean-definition-overriding: true
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/user?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&useSSL=false&zeroDateTimeBehavior=convertToNull&serverTimezone=Asia/Shanghai&allowMultiQueries=true
username: root
password: root
创建 TestController类
/**
* @author changq
*/
@Slf4j
@RestController
public class TestController {
@GetMapping("/get")
JSONObject get() {
JSONObject json = new JSONObject();
json.put("data", "get信息成功");
return json;
}
}
创建 LoginUser类 实现 UserDetails
/**
* @author changq
*/
@Data
@TableName("sys_user")
public class LoginUser implements UserDetails {
@TableId(type = IdType.ASSIGN_UUID)
private String id;
private String username;
private String password;
private String mobile;
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return null;
}
@Override
public boolean isAccountNonExpired() {
return true;//需要为true,不然认证失败
}
@Override
public boolean isAccountNonLocked() {
return true;//需要为true,不然认证失败
}
@Override
public boolean isCredentialsNonExpired() {
return true;//需要为true,不然认证失败
}
@Override
public boolean isEnabled() {
return true;//需要为true,不然认证失败
}
}
创建 MyUserDetailsService 类 继承 UserDetailsService 方便后面扩展
扩展了简单的手机号密码登录,没用验证码,验证码的自行扩展
/**
* @author changq
*/
public interface MyUserDetailsService extends UserDetailsService, IService<LoginUser> {
LoginUser loadUserByMobile(String username) throws UsernameNotFoundException;
}
创建 MyUserDetailsServiceImpl 类 实现 MyUserDetailsService
/**
* UserDetailsService 实现
*
* @author changq
*/
@Slf4j
@Service
public class MyUserDetailsServiceImpl extends ServiceImpl<UserMapper, LoginUser> implements MyUserDetailsService {
// @Autowired
// PasswordEncoder passwordEncoder;
@Override
public LoginUser loadUserByUsername(String username) throws UsernameNotFoundException {
//判断用户名是否为空
if (StrUtil.isEmpty(username)) {
throw new NullPointerException("用户名不能为空");
}
LoginUser loginUser = this.getOne(Wrappers.<LoginUser>lambdaQuery().eq(LoginUser::getUsername, username));
log.info("获取到的用户:{}", loginUser);
if (Objects.isNull(loginUser)) {
throw new InternalAuthenticationServiceException("用户名错误");
}
return loginUser;
}
@Override
public LoginUser loadUserByMobile(String mobile) throws UsernameNotFoundException {
LoginUser loginUser = this.getOne(Wrappers.<LoginUser>lambdaQuery().eq(LoginUser::getMobile, mobile));
log.info("手机号获取到的用户:{}", loginUser);
if (Objects.isNull(loginUser)) {
throw new InternalAuthenticationServiceException("手机号错误");
}
return loginUser;
}
}
创建 MyMobileAuthenticationFilter类
/**
* 1.手机号认证过滤器
*
* @author changq
*/
public class MyMobileAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
public static final String SPRING_SECURITY_FORM_USERNAME_KEY = "mobile";
public static final String SPRING_SECURITY_FORM_PASSWORD_KEY = "password";
// 定义登录路径
private static final AntPathRequestMatcher DEFAULT_ANT_PATH_REQUEST_MATCHER = new AntPathRequestMatcher("/mobile/toLogin",
"POST");
private String usernameParameter = SPRING_SECURITY_FORM_USERNAME_KEY;
private String passwordParameter = SPRING_SECURITY_FORM_PASSWORD_KEY;
private boolean postOnly = true;
public MyMobileAuthenticationFilter() {
super(DEFAULT_ANT_PATH_REQUEST_MATCHER);
}
public MyMobileAuthenticationFilter(AuthenticationManager authenticationManager) {
super(DEFAULT_ANT_PATH_REQUEST_MATCHER, authenticationManager);
}
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
throws AuthenticationException {
if (this.postOnly && !request.getMethod().equals("POST")) {
throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
}
String username = obtainUsername(request);
username = (username != null) ? username.trim() : "";
String password = obtainPassword(request);
password = (password != null) ? password : "";
MobileAuthenticationToken authRequest = MobileAuthenticationToken.unauthenticated(username,
password);
// Allow subclasses to set the "details" property
setDetails(request, authRequest);
return this.getAuthenticationManager().authenticate(authRequest);
}
/**
* Enables subclasses to override the composition of the password, such as by
* including additional values and a separator.
* <p>
* This might be used for example if a postcode/zipcode was required in addition to
* the password. A delimiter such as a pipe (|) should be used to separate the
* password and extended value(s). The <code>AuthenticationDao</code> will need to
* generate the expected password in a corresponding manner.
* </p>
*
* @param request so that request attributes can be retrieved
* @return the password that will be presented in the <code>Authentication</code>
* request token to the <code>AuthenticationManager</code>
*/
@Nullable
protected String obtainPassword(HttpServletRequest request) {
return request.getParameter(this.passwordParameter);
}
/**
* Enables subclasses to override the composition of the username, such as by
* including additional values and a separator.
*
* @param request so that request attributes can be retrieved
* @return the username that will be presented in the <code>Authentication</code>
* request token to the <code>AuthenticationManager</code>
*/
@Nullable
protected String obtainUsername(HttpServletRequest request) {
return request.getParameter(this.usernameParameter);
}
/**
* Provided so that subclasses may configure what is put into the authentication
* request's details property.
*
* @param request that an authentication request is being created for
* @param authRequest the authentication request object that should have its details
* set
*/
protected void setDetails(HttpServletRequest request, MobileAuthenticationToken authRequest) {
authRequest.setDetails(this.authenticationDetailsSource.buildDetails(request));
}
/**
* Sets the parameter name which will be used to obtain the username from the login
* request.
*
* @param usernameParameter the parameter name. Defaults to "username".
*/
public void setUsernameParameter(String usernameParameter) {
Assert.hasText(usernameParameter, "Username parameter must not be empty or null");
this.usernameParameter = usernameParameter;
}
/**
* Sets the parameter name which will be used to obtain the password from the login
* request..
*
* @param passwordParameter the parameter name. Defaults to "password".
*/
public void setPasswordParameter(String passwordParameter) {
Assert.hasText(passwordParameter, "Password parameter must not be empty or null");
this.passwordParameter = passwordParameter;
}
/**
* Defines whether only HTTP POST requests will be allowed by this filter. If set to
* true, and an authentication request is received which is not a POST request, an
* exception will be raised immediately and authentication will not be attempted. The
* <tt>unsuccessfulAuthentication()</tt> method will be called as if handling a failed
* authentication.
* <p>
* Defaults to <tt>true</tt> but may be overridden by subclasses.
*/
public void setPostOnly(boolean postOnly) {
this.postOnly = postOnly;
}
public final String getUsernameParameter() {
return this.usernameParameter;
}
public final String getPasswordParameter() {
return this.passwordParameter;
}
}
创建 MobileAuthenticationToken 类
/**
* 4.手机号认证token
*
* @author changq
*/
public class MobileAuthenticationToken extends AbstractAuthenticationToken {
private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;
private final Object principal;
private Object credentials;
public MobileAuthenticationToken(Object principal, Object credentials) {
super(null);
this.principal = principal;
this.credentials = credentials;
setAuthenticated(false);
}
/**
* @param principal
* @param credentials
* @param authorities
*/
public MobileAuthenticationToken(Object principal, Object credentials,
Collection<? extends GrantedAuthority> authorities) {
super(authorities);
this.principal = principal;
this.credentials = credentials;
super.setAuthenticated(true); // must use super, as we override
}
/**
* @param principal
* @param credentials
*/
public static MobileAuthenticationToken unauthenticated(Object principal, Object credentials) {
return new MobileAuthenticationToken(principal, credentials);
}
/**
* @param principal
* @param credentials
*/
public static MobileAuthenticationToken authenticated(Object principal, Object credentials,
Collection<? extends GrantedAuthority> authorities) {
return new MobileAuthenticationToken(principal, credentials, authorities);
}
@Override
public Object getCredentials() {
return this.credentials;
}
@Override
public Object getPrincipal() {
return this.principal;
}
@Override
public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException {
Assert.isTrue(!isAuthenticated,
"Cannot set this token to trusted - use constructor which takes a GrantedAuthority list instead");
super.setAuthenticated(false);
}
@Override
public void eraseCredentials() {
super.eraseCredentials();
this.credentials = null;
}
}
创建 MyMobileAuthenticationProvider类
/**
* 3.手机号登录身份认证器
*
* @author changq
*/
@Slf4j
@Component
@Setter
public class MyMobileAuthenticationProvider implements AuthenticationProvider {
private PasswordEncoder passwordEncoder;
private MyUserDetailsService userDetailsService;
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
String phone = authentication.getPrincipal().toString();
String password = authentication.getCredentials().toString();
log.info("手机号:{}--,密码:{}", phone, password);
UserDetails loginUser = userDetailsService.loadUserByMobile(phone);
if (passwordEncoder.matches(password, loginUser.getPassword())) {
MobileAuthenticationToken mobileAuthenticationToken = new MobileAuthenticationToken(loginUser, password, loginUser.getAuthorities());
mobileAuthenticationToken.setDetails(authentication.getDetails());
return mobileAuthenticationToken;
} else {
throw new BadCredentialsException("密码错误");
}
}
@Override
public boolean supports(Class<?> authentication) {
return MobileAuthenticationToken.class.isAssignableFrom(authentication);
}
创建 MyAuthenticationSuccessHandler类
/**
* @author changq
* 认证成功处理
*/
@Slf4j
@Component
public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
log.info("认证成功处理------------");
response.setContentType("application/json;charset=utf-8");
JSONObject jsonObject = new JSONObject();
jsonObject.put("code", 200);
jsonObject.put("msg", "认证成功");
Map<String, Object> map = new HashMap<>();
map.put("user", authentication.getPrincipal());
String token = JWTUtil.createToken(map, "123".getBytes());
jsonObject.put("token", token);
PrintWriter writer = response.getWriter();
writer.write(jsonObject.toJSONString());
}
}
创建 MyAuthenticationFailureHandler类
/**
* @author changq
* 认证失败处理
*/
@Slf4j
@Component
public class MyAuthenticationFailureHandler implements AuthenticationFailureHandler {
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
log.info("认证失败处理!!!");
response.setContentType("application/json;charset=utf-8");
JSONObject jsonObject = new JSONObject();
jsonObject.put("code", 403);
jsonObject.put("data", "");
jsonObject.put("msg", exception.getMessage());
PrintWriter writer = response.getWriter();
writer.write(jsonObject.toJSONString());
}
}
创建 MyMobileSecurityConfig类
/**
* 2.手机号登录相关Security安全配置
*
* @author changq
*/
@Component
public class MyMobileSecurityConfig extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {
@Autowired
private PasswordEncoder passwordEncoder;
@Autowired
private MyUserDetailsService myUserDetailsService;
@Override
public void configure(HttpSecurity httpSecurity) throws Exception {
MyMobileAuthenticationProvider provider = new MyMobileAuthenticationProvider();
//使用自己的 UserDetailsService
provider.setUserDetailsService(myUserDetailsService);
//使用自定义的 passwordEncoder
provider.setPasswordEncoder(passwordEncoder);
httpSecurity.authenticationProvider(provider);
MyMobileAuthenticationFilter myMobileAuthenticationFilter = new MyMobileAuthenticationFilter();
myMobileAuthenticationFilter.setAuthenticationManager(httpSecurity.getSharedObject(AuthenticationManager.class));
myMobileAuthenticationFilter.setAuthenticationSuccessHandler(new MyAuthenticationSuccessHandler());
myMobileAuthenticationFilter.setAuthenticationFailureHandler(new MyAuthenticationFailureHandler());
httpSecurity.addFilterAfter(myMobileAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
}
}
创建 RequestTokenFilter 类 校验token
/**
* 校验请求token
* 每个请求都需要校验
*
* @author changq
*/
@Slf4j
//@Component
public class RequestTokenFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException {
log.info("进入token校验");
SecurityContextHolder.clearContext();
String token = request.getHeader("token");
if (StrUtil.isEmpty(token)) {
chain.doFilter(request, response);
return;
}
JWT jwt;
try {
jwt = JWTUtil.parseToken(token);
} catch (Exception e) {
chain.doFilter(request, response);
return;
}
Object user = jwt.getPayload("user");
LoginUser loginUser = JSON.parseObject(user.toString(), LoginUser.class);
log.info("用户信息:{}", loginUser);SecurityContextHolder.getContext().setAuthentication(UsernamePasswordAuthenticationToken.authenticated(loginUser, loginUser.getPassword(), loginUser.getAuthorities()));
chain.doFilter(request, response);
}
创建 MyAuthenticationEntryPointHandler 类
/**
* 请求接口 401未授权处理
*
* @author changq
*/
@Slf4j
@Component
public class MyAuthenticationEntryPointHandler implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
log.info("401未授权处理");
response.setContentType("application/json;charset=utf-8");
String s = String.format("请求:%s,认证失败,无法访问该资源", request.getRequestURI());
response.getWriter().write(JSON.toJSONString(R.error(HttpStatus.HTTP_UNAUTHORIZED, s, "")));
}
}
最后完善第一步的 WebSecurityConfig 配置
@Bean
SecurityFilterChain defaultSecurityFilterChain(HttpSecurity httpSecurity) throws Exception {
httpSecurity
//开启跨域
// .cors().and()
//禁用CSRF,因为不使用 session,前后端分离项目不需要
.csrf().disable()
//禁用session,前后端分离项目基于token不需要
.sessionManagement(AbstractHttpConfigurer::disable)
.authorizeRequests(
authorizeRequests -> authorizeRequests.antMatchers("/toLogin").permitAll()
.anyRequest().authenticated()
);
httpSecurity.apply(myMobileSecurityConfig);
httpSecurity.exceptionHandling().authenticationEntryPoint(authenticationEntryPoint);
httpSecurity.addFilterBefore(new RequestTokenFilter(), UsernamePasswordAuthenticationFilter.class);
return httpSecurity.build();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
至此,简单的自定义登录实现就完成了,我们来测试一下
执行登录接口 localhost:8080/mobile/login,随便输入手机号,返回403
输入正确的手机号,返回token
我们不带token 调用一下 /get 接口,返回401
携带 token 请求 /get 接口
以上只是个人的一些理解和简单实现,如有问题请指教!!
谢谢!!!!