springboot2.7整合springSecurity

springboot2.7之后自动配置的变化 了解springboot2.7之后自动配置发生的变化,以及自动装配的原理 阅读详情

springboot2.7整合springSecurity

0.简介

本着前人栽树,后人乘凉的这种思想,自己花了一些时间,用心的整理了一套springboot整合springsecurity的教程。

该教程是基于springboot2.7.3版本开发的,在2.7以上版本中,springSecurity已经废弃了WebSecurityConfigurerAdapter,而是使用 bean 注入的方式,详情可参阅官方文档:https://spring.io/blog/2022/02/21/spring-security-without-the-websecurityconfigureradapter#ldap-authentication

该教程是基于前后端分离,会实现以下两种登录功能:

  1. 用户名+密码+图片验证码

  1. 手机号登录

这两种方式可以同时存在,并且互不干预。

本教程会通过阅读其内置的用户名密码登录的源码,以及结合官网文档来实现一些自定义的登录方式。

注:教程主要是开发思路的讲解,其中涉及到的代码仅供参考,为了方便,很多地方忽略了一些细节

1.快速开始

1.1创建工程

创建一个名为springsecurity的工作,springboot的版本选择2.7.3

1.2引入springsecurity依赖

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

    <!--security依赖-->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-security</artifactId>
    </dependency>

    <!--测试相关-->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
</dependencies>

启动程序,在浏览器中输入localhost:你的端口号,就会出现如下的界面

输入用户名和密码即可登录,这里的用户名默认是user,密码则会打印在控制台上

2.原理初探

2.1springSecurity的作用

Spring Security是一个提供了认证、授权和防止常见攻击的框架。

认证:验证当前访问系统的是不是本系统的用户,简单的讲就是登录

授权:经过认证的用户是否有权限进行某个操作

2.2springSecurity的本质

springSecurity提供了许多基础的过滤器,当客户端向应用发送请求,会经过容器中已经创建一个过滤链(许多过滤器),如图:

springSecurity内置了哪些过滤器?

这是官方文档截图的一部分,详细的可以看官方文档:

https://docs.spring.io/spring-security/reference/5.7.3/servlet/architecture.html

另外我们在启动程序的时候,也可以看到springSecurity创建了哪些过滤器:

2.3表单登录流程

一般前后端不分离的项目,登录流程如图

用户提交用户名和密码,服务器会进行用户名密码比对,如果失败,会跳转到登录页,重新登录,如果成功,会把用户信息保存到session中,并且跳转到能访问的首页

2.4走进源码

阅读哪一块的源码?怎么入手?

从官方文档的截图,从图可以知道,用户名密码验证的主要是从UsernamePasswordAuthenticationFilter这个过滤器开始的。

2.5AbstractAuthenticationProcessingFilter

打开源码可以看到UsernamePasswordAuthenticationFilter是继承AbstractAuthenticationProcessingFilter,因此我们就从它开始阅读。

AbstractAuthenticationProcessingFilter是基于基于浏览器的HTTP身份验证请求的抽象处理器,实现了整个身份的验证过程,下面会阅读重点部分的代码

private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
			throws IOException, ServletException {
	//(1)如果不是登录验证请求,就直接放行,执行下一个过滤器
    if (!requiresAuthentication(request, response)) {
        chain.doFilter(request, response);
        return;
    }
    try {
        //(2)登录验证的核心方法,具体实现是在UsernamePasswordAuthenticationFilter
        Authentication authenticationResult = attemptAuthentication(request, response);
        if (authenticationResult == null) {
            return;
        }
        this.sessionStrategy.onAuthentication(authenticationResult, request, response);

        if (this.continueChainBeforeSuccessfulAuthentication) {
            chain.doFilter(request, response);
        }

        //(3)验证成功后的处理逻辑
        successfulAuthentication(request, response, chain, authenticationResult);
    }
    catch (InternalAuthenticationServiceException failed) {
        this.logger.error("An internal error occurred while trying to authenticate the user.", failed);
        unsuccessfulAuthentication(request, response, failed);
    }
    catch (AuthenticationException ex) {
        //(4)验证失败后的处理逻辑
        unsuccessfulAuthentication(request, response, ex);
    }
}

这一段代码是整个认证的核心,

(1)判断用户的请求是不是登录认证的请求,例如我们下面的认证接口"/admin/login"

(2)登录验证的核心方法,里面实现了用户名密码认证

(3)验证成功后的处理逻辑

(4)验证失败后的处理逻辑

我们先来分析一下验证成功后的处理逻辑

successfulAuthentication方法

protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain,
       Authentication authResult) throws IOException, ServletException {
    //(1)SecurityContextHolder类很重要,主要是创建一个线程池保存用户信息,后面接口要使用到用户信息时就可以通过这个类去获取
    SecurityContext context = SecurityContextHolder.createEmptyContext();
    context.setAuthentication(authResult);
    SecurityContextHolder.setContext(context);
    this.securityContextRepository.saveContext(context, request, response);
    if (this.logger.isDebugEnabled()) {
        this.logger.debug(LogMessage.format("Set SecurityContextHolder to %s", authResult));
    }
    //这里是记住登录状态的功能
    this.rememberMeServices.loginSuccess(request, response, authResult);
    if (this.eventPublisher != null) {
        this.eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(authResult, this.getClass()));
    }
    //(2)登录成功后的相关处理,例如跳转到首页
    this.successHandler.onAuthenticationSuccess(request, response, authResult);
}

我们可以看看this.successHandler是什么,

private AuthenticationSuccessHandler successHandler = new SavedRequestAwareAuthenticationSuccessHandler();

private AuthenticationFailureHandler failureHandler = new SimpleUrlAuthenticationFailureHandler();

我们可以看到this.successHandler初始化值是SavedRequestAwareAuthenticationSuccessHandler,我们进这个类看看onAuthenticationSuccess方法

@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
                                    Authentication authentication) throws ServletException, IOException {
    SavedRequest savedRequest = this.requestCache.getRequest(request, response);
    if (savedRequest == null) {
        super.onAuthenticationSuccess(request, response, authentication);
        return;
    }
    String targetUrlParameter = getTargetUrlParameter();
    if (isAlwaysUseDefaultTargetUrl()
        || (targetUrlParameter != null && StringUtils.hasText(request.getParameter(targetUrlParameter)))) {
        this.requestCache.removeRequest(request, response);
        super.onAuthenticationSuccess(request, response, authentication);
        return;
    }
    clearAuthenticationAttributes(request);
    // Use the DefaultSavedRequest URL
    String targetUrl = savedRequest.getRedirectUrl();
    getRedirectStrategy().sendRedirect(request, response, targetUrl);
}

这段逻辑主要是登录成功后跳转到哪一个页面。

之所以会讲这一段,主要是因为我们后面自定义自己的登录方式会涉及到。例如前后端登录成功后,使用redis保存用户信息以及生产一窜token返回给前端。

这时我们可以参考SavedRequestAwareAuthenticationSuccessHandler,创建自己的AuthenticationSuccessHandler,然后赋值给this.successHandler

验证失败后的处理逻辑这块就不讲源码了,它的原理跟成功是一样的,后面我们自定义一个失败的handler去替换就好。

2.6UsernamePasswordAuthenticationFilter

UsernamePasswordAuthenticationFilter是继承AbstractAuthenticationProcessingFilter这个抽象类的,里面有一个非常重要的方法attemptAuthentication,里面实现了身份验证处理的逻辑

@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
    throws AuthenticationException {
    //(1)判断是否是post提交
    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 : "";
    //(2)根据用户名和密码生成一个身份认证令牌,其实就是保存一些用户登录的信息
    UsernamePasswordAuthenticationToken authRequest = UsernamePasswordAuthenticationToken.unauthenticated(username,
                                                                                                          password);
    setDetails(request, authRequest);
    //(3)调用这个AuthenticationManager接口类中的authenticate验证接口
    return this.getAuthenticationManager().authenticate(authRequest);
}

根据用户名和密码生成一个身份认证令牌,然后把令牌传到 AuthenticationManager 进行验证。我们先来看看

UsernamePasswordAuthenticationToken

public class UsernamePasswordAuthenticationToken extends AbstractAuthenticationToken {

	private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;
	//用户信息
	private final Object principal;
	//用户凭证
	private Object credentials;
	
	public UsernamePasswordAuthenticationToken(Object principal, Object credentials) {
		super(null);
		this.principal = principal;
		this.credentials = credentials;
		setAuthenticated(false);
	}

	public UsernamePasswordAuthenticationToken(Object principal, Object credentials,
			Collection<? extends GrantedAuthority> authorities) {
        //这里是权限信息
		super(authorities);
		this.principal = principal;
		this.credentials = credentials;
		super.setAuthenticated(true);
	}
	
    //(1)第一次验证时调用这个方法
	public static UsernamePasswordAuthenticationToken unauthenticated(Object principal, Object credentials) {
		return new UsernamePasswordAuthenticationToken(principal, credentials);
	}
	
    //(2)已经验证过后可以调用此方法
	public static UsernamePasswordAuthenticationToken authenticated(Object principal, Object credentials,
			Collection<? extends GrantedAuthority> authorities) {
		return new UsernamePasswordAuthenticationToken(principal, credentials, authorities);
	}
}

这个类比较意思,principal和credentials数据类型是object,接着往下看,后面会有我们的答案。

现在重点看看this.getAuthenticationManager().authenticate(authRequest)

我们打开AuthenticationManager

public interface AuthenticationManager {
	Authentication authenticate(Authentication authentication) throws AuthenticationException;
}

这只是一个接口,其实现是在ProviderManager

2.7ProviderManager

springsecurity会提供许多AuthenticationProvider,而ProviderManager这个类就处理这些AuthenticationProvider

public class ProviderManager implements AuthenticationManager, MessageSourceAware, InitializingBean {
    @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();
        //在容器启动时,springsecurity已经初始化了一些provider,其中就有DaoAuthenticationProvider,其主要是用户名和密码验证的
		for (AuthenticationProvider provider : getProviders()) {
			if (!provider.supports(toTest)) {
                //如果provider不支持UsernamePasswordAuthenticationToken,就跳到下一个provider
				continue;
			}
			if (logger.isTraceEnabled()) {
				logger.trace(LogMessage.format("Authenticating request with %s (%d/%d)",
						provider.getClass().getSimpleName(), ++currentPosition, size));
			}
			try {
                //(1)DaoAuthenticationProvider就是这个provider
				result = provider.authenticate(authentication);
				if (result != null) {
					copyDetails(authentication, result);
					break;
				}
			}
			catch (AccountStatusException | InternalAuthenticationServiceException ex) {
				prepareException(ex, authentication);
				throw ex;
			}
			catch (AuthenticationException ex) {
				lastException = ex;
			}
		}
		if (result == null && this.parent != null) {
			try {
				parentResult = this.parent.authenticate(authentication);
				result = parentResult;
			}
			catch (ProviderNotFoundException ex) {
			}
			catch (AuthenticationException ex) {
				parentException = ex;
				lastException = ex;
			}
		}
		if (result != null) {
			if (this.eraseCredentialsAfterAuthentication && (result instanceof CredentialsContainer)) {
				((CredentialsContainer) result).eraseCredentials();
			}
			if (parentResult == null) {
				this.eventPublisher.publishAuthenticationSuccess(result);
			}

			return result;
		}

		if (lastException == null) {
			lastException = new ProviderNotFoundException(this.messages.getMessage("ProviderManager.providerNotFound",
					new Object[] { toTest.getName() }, "No AuthenticationProvider found for {0}"));
		}
		if (parentException == null) {
			prepareException(lastException, authentication);
		}
		throw lastException;
	}
}

这个类的内容不是重点,重点是这个DaoAuthenticationProvider,而它又是继承AbstractUserDetailsAuthenticationProvider这个抽象类,因此我们先来了解一下它

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;
        //(1)根据用户名从缓存中获取用户信息
		UserDetails user = this.userCache.getUserFromCache(username);
		if (user == null) {
			cacheWasUsed = false;
			try {
                //(2)调用实现类的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 {
           	//(3)判断用户状态,是否是启用
			this.preAuthenticationChecks.check(user);
            //(4)密码比较
			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();
		}
        //(5)创建一个成功的Authentication对象
		return createSuccessAuthentication(principalToReturn, authentication, user);
	}
}

这里面实现的逻辑是这样的

(2)调用实现类的DaoAuthenticationProvider的retrieveUser,获取到保存在内存中的用户信息,如密码,账号状态等

(3)判断用户状态是否有效

(4)获取的密码跟提交过来的密码是否一致

打开DaoAuthenticationProvider,再看看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里面的loadUserByUsername接口查询用户信息,然后进行密码校验。

因此后面我们在做手机登录的时候,就可以自定义一个provider,然后调用一个根据手机号查询用户信息的接口即可。

接下来重点看看这一段

//最终实现查询用户信息的代码在这里
UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);

2.8InMemoryUserDetailsManager

public interface UserDetailsService {
	UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}

loadUserByUsername是接口UserDetailsService里面的一个方法,是通过用户名获取用户信息,其实现是在InMemoryUserDetailsManager

public class InMemoryUserDetailsManager implements UserDetailsManager, UserDetailsPasswordService {
    @Override
	public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        //(1)从缓存中获取用户信息
		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());
	}
}

springSecurity获取用户信息,默认从缓存中换取的,然后返回一个UserDetails,我们看看其定义的User类,下面只是列出用部分

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;
	}
}

首先这个类继承UserDetails了,定义了一些基本信息,最重要的是用户名、密码、权限。

后面我们会重写这部分逻辑,我们会重写loadUserByUsername,使其查询我们的数据库。

2.9SecurityContextPersistenceFilter

当登录成功后,再次发送请求时,springsecurity时如何知道该用户已经通过验证的?

这时我们就要认识一下SecurityContextPersistenceFilter

它的作用主要是: 创建SecurityContext安全上下文信息和请求结束时清空SecurityContextHolder

接下来看源码

@Deprecated
public class SecurityContextPersistenceFilter extends GenericFilterBean {
    @Override
	public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
			throws IOException, ServletException {
		doFilter((HttpServletRequest) request, (HttpServletResponse) response, chain);
	}

	private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
			throws IOException, ServletException {
		//(1)同一个请求只处理一次
		if (request.getAttribute(FILTER_APPLIED) != null) {
			chain.doFilter(request, response);
			return;
		}
		request.setAttribute(FILTER_APPLIED, Boolean.TRUE);
		if (this.forceEagerSessionCreation) {
            //(2)创建一个HttpSession
			HttpSession session = request.getSession();
			if (this.logger.isDebugEnabled() && session.isNew()) {
				this.logger.debug(LogMessage.format("Created session %s eagerly", session.getId()));
			}
		}
		HttpRequestResponseHolder holder = new HttpRequestResponseHolder(request, response);
        //(3)获取登录用户的相关信息,如果没有登录,用户信息为空
		SecurityContext contextBeforeChainExecution = this.repo.loadContext(holder);
		try {
            //(4)SecurityContextHolder绑定SecurityContext对象
			SecurityContextHolder.setContext(contextBeforeChainExecution);
			if (contextBeforeChainExecution.getAuthentication() == null) {
				logger.debug("Set SecurityContextHolder to empty SecurityContext");
			}
			else {
				if (this.logger.isDebugEnabled()) {
					this.logger
							.debug(LogMessage.format("Set SecurityContextHolder to %s", contextBeforeChainExecution));
				}
			}
            //(5)下一个过滤器处理
			chain.doFilter(holder.getRequest(), holder.getResponse());
		}
		finally {
			SecurityContext contextAfterChainExecution = SecurityContextHolder.getContext();
			// (5)在任何其他操作之前删除SecurityContextHolder内容
			SecurityContextHolder.clearContext();
            //(6)重新保存用户信息
			this.repo.saveContext(contextAfterChainExecution, holder.getRequest(), holder.getResponse());
			request.removeAttribute(FILTER_APPLIED);
			this.logger.debug("Cleared SecurityContextHolder to complete request");
		}
	}
}

到现在为止,登录验证的主要源码已经介绍完毕,如果没有看明白,不要紧,只要有个印象就行,接下来自定义自己的登录接口时,会仿照其源码。

3.准备工作

3.1创建数据表

/*用户角色映射表*/
CREATE TABLE `sys_user_role`  (
  `id` bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT,
  `user_id` bigint(200) NOT NULL COMMENT '用户id',
  `role_id` bigint(200) NOT NULL DEFAULT 0 COMMENT '角色id',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 2 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic;

/*用户表*/
CREATE TABLE `sys_user`  (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键',
  `user_name` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT 'NULL' COMMENT '用户名',
  `nick_name` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT 'NULL' COMMENT '昵称',
    `phone` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT 'NULL' COMMENT '电话',
  `password` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT 'NULL' COMMENT '密码',
  `status` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '0' COMMENT '账号状态(0正常 1停用)',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 3 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '用户表' ROW_FORMAT = Dynamic;

/*角色菜单映射表*/
CREATE TABLE `sys_role_menu`  (
  `id` bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT,
  `role_id` bigint(200) NOT NULL COMMENT '角色ID',
  `menu_id` bigint(200) NOT NULL DEFAULT 0 COMMENT '菜单id',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 2 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic;

/*角色表*/
CREATE TABLE `sys_role`  (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `role_name` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 2 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '角色表' ROW_FORMAT = Dynamic;

/*菜单表*/
CREATE TABLE `sys_menu`  (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '自增ID',
  `menu_name` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT 'NULL' COMMENT '菜单名',
  `perms` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '权限标识',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 2 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '菜单表' ROW_FORMAT = Dynamic;

新建一条用户数据:账号:test 密码:123456

INSERT INTO `springsecurity`.`sys_user`(`user_name`, `nick_name`, `password`, `phone`, `status`) VALUES ('test', '1', '$2a$12$pgFnH5Ot.XIvbaTM7X9nNe8AGwBV.3eggszusKShXXG2HJ1fFdNMO', '13662301000', '0');

3.2生成代码

本教程使用mybatis-plus

引入数据库相关依赖:

<!--操作mysql的相关依赖-->
<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-boot-starter</artifactId>
    <version>3.5.3</version>
</dependency>

<dependency>
    <groupId>com.mysql</groupId>
    <artifactId>mysql-connector-j</artifactId>
    <version>8.0.32</version>
</dependency>

<!--代码生成器相关依赖-->
<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-generator</artifactId>
    <version>3.5.3</version>
</dependency>

<dependency>
    <groupId>org.freemarker</groupId>
    <artifactId>freemarker</artifactId>
    <version>2.3.28</version>
</dependency>

配置:

server.port=8002

spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/springsecurity?Unicode=true&characterEncoding=utf8&serverTimezone=UTC
spring.datasource.username=root
spring.datasource.password=root

生成方法:

package com.example.springsecuritydemo1;

import com.baomidou.mybatisplus.generator.FastAutoGenerator;
import com.baomidou.mybatisplus.generator.config.OutputFile;
import com.baomidou.mybatisplus.generator.config.TemplateType;
import com.baomidou.mybatisplus.generator.engine.FreemarkerTemplateEngine;
import org.apache.ibatis.annotations.Mapper;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;

import java.util.Collections;

@SpringBootTest
class AutoGeneratorTests {

    @Test
    void contextLoads() {
        FastAutoGenerator.create("jdbc:mysql://127.0.0.1:3306/springsecurity","root","root")
                .globalConfig(builder -> {
                    builder.author("myh") // 设置作者
                            .outputDir("F:\\javaStudy\\springsecurity-demo1\\src\\main\\java"); // 指定输出目录
                })
                .packageConfig(builder -> {
                    builder.parent("com.example.springsecuritydemo1") // 设置父包名
                            .moduleName("user") // 设置父包模块名
                            .entity("entity")
                            .service("service")
                            .serviceImpl("service.impl")
                            .mapper("mapper")
                            .xml("mapper.xml")
                            .controller("controller")
                            .pathInfo(Collections.singletonMap(OutputFile.xml, "F:\\javaStudy\\springsecurity-demo1\\src\\main\\resources\\mapper")); // 设置mapperXml生成路径
                })
                .templateConfig(builder -> {
                    builder.disable(TemplateType.ENTITY)
                            .entity("/templates/entity.java")
                            .service("/templates/service.java")
                            .serviceImpl("/templates/serviceImpl.java")
                            .mapper("/templates/mapper.java")
                            .controller("/templates/controller.java");
                })
                .strategyConfig(builder -> {
                    builder.controllerBuilder().enableRestStyle();
                    builder.entityBuilder().enableLombok();
                    builder.mapperBuilder().mapperAnnotation(Mapper.class);
                    builder.serviceBuilder().formatServiceFileName("Service");
                    builder.serviceBuilder().formatServiceImplFileName("ServiceImp");
                    builder.addInclude("sys_menu") // 设置需要生成的表名
                            .addInclude("sys_user_role").addInclude("sys_user").addInclude("sys_role").addInclude("sys_role_menu");
                    // .addTablePrefix("sys_"); // 设置过滤表前缀

                })
                .templateEngine(new FreemarkerTemplateEngine()) // 使用Freemarker引擎模板,默认的是Velocity引擎模板
                .execute();
    }

}

3.3其他

主要使用hutool这个基础工具类

引入redis及相关工具类

<!--hutool基础工具类-->
<dependency>
    <groupId>cn.hutool</groupId>
    <artifactId>hutool-all</artifactId>
    <version>5.7.20</version>
</dependency>

<dependency>
    <groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
    <version>3.7.0</version>
</dependency>

<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <optional>true</optional>
</dependency>

<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>fastjson</artifactId>
    <version>1.2.33</version>
</dependency>

<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-lang3</artifactId>
    <version>3.10</version>
</dependency>
<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-collections4</artifactId>
    <version>4.0</version>
</dependency>

redis配置

#-------------------------------------------------------------------------------
# Redis客户端配置样例
# 每一个分组代表一个Redis实例
# 无分组的Pool配置为所有分组的共用配置,如果分组自己定义Pool配置,则覆盖共用配置
# 池配置来自于:https://www.cnblogs.com/jklk/p/7095067.html
#-------------------------------------------------------------------------------

#----- 默认(公有)配置
# 地址,默认localhost
host = localhost
# 端口,默认6379
port = 6379
# 超时,默认2000
timeout = 2000
# 连接超时,默认timeout
connectionTimeout = 2000
# 读取超时,默认timeout
soTimeout = 2000
# 密码,默认无
password = 12345
# 数据库序号,默认0
database = 0
# 客户端名,默认"Hutool"
clientName = Hutool
# SSL连接,默认false
ssl = false;

统一返回值类:

package com.demo.springsecurity.common.util;

import lombok.Data;

/**
 * @Description:
 * @Author: mayanhui
 * @Date: 2023/2/7 9:38
 */
@Data
public class ResultUtil<T> {
    private Integer code;
    private String message;
    private T data;

    public ResultUtil() {
        this.code = 200;
        this.message = "success";
    }

    public ResultUtil(Integer code, String msg) {
        this.code = code;
        this.message = msg;
    }

    public ResultUtil(Integer code, T data) {
        this.code = code;
        this.data = data;
    }

    public ResultUtil(T data) {
        this.code = 200;
        this.message = "success";
        this.data = data;
    }

    public static <T> ResultUtil<T> ok(){
        return new ResultUtil<T>();
    }

    public static <T> ResultUtil<T> ok(T data){
        return new ResultUtil<T>(data);
    }

    public static <T> ResultUtil<T> ok(Integer code,String message){
        return new ResultUtil<T>(code,message);
    }

    public static <T> ResultUtil<T> fail(String message){
        return new ResultUtil<T>(500,message);
    }

    public static <T> ResultUtil<T> fail(Integer code,String message){
        return new ResultUtil<T>(code,message);
    }
}

全局异常及自定义异常封装

package com.demo.springsecurity.common.exception;

import com.demo.springsecurity.common.util.ResultUtil;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestControllerAdvice;

/**
 * @Description:
 * @Author: mayanhui
 * @Date: 2023/2/7 18:13
 */
@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(Exception.class)
    public ResultUtil<Void> error(Exception e){
        return ResultUtil.fail(e.getMessage());
    }

    @ExceptionHandler(ServiceException.class)
    @ResponseBody
    public ResultUtil<Void> serviceException(ServiceException e){
        return ResultUtil.fail(e.getCode(),e.getMsg());
    }
}
package com.demo.springsecurity.common.exception;

import lombok.Data;

/**
 * @Description:
 * @Author: mayanhui
 * @Date: 2023/2/7 18:16
 */
@Data
public class ServiceException extends RuntimeException{
    private Integer code;
    private String msg;

    public ServiceException(Integer code,String msg){
        super(msg);
        this.code = code;
        this.msg = msg;
    }

    public ServiceException(String msg){
        super(msg);
        this.code = 500;
        this.msg = msg;
    }

}

4.用户名密码登录验证

4.1登录校验流程

这张图网上抄的,我们前后端分离的实现过程就按照这个流程

4.2图片验证码

@GetMapping("admin/code")
public void getHtoolVerifyCode(HttpServletResponse response) throws IOException {
    RandomGenerator randomGenerator = new RandomGenerator("0123456789", 4);
    LineCaptcha lineCaptcha = CaptchaUtil.createLineCaptcha(200, 100);
    lineCaptcha.setGenerator(randomGenerator);
    lineCaptcha.createCode();
    Jedis jedis = RedisDS.create().getJedis();
    jedis.set(RedisKey.ADMIN_VERIFY_CODE +lineCaptcha.getCode(),lineCaptcha.getCode());
    response.setContentType(MediaType.IMAGE_PNG_VALUE);
    lineCaptcha.write(response.getOutputStream());
}

使用hutool生成验证码,并且存储到缓存里面,注意key的规则 特殊字符+当前验证码作为key

开头的时候已经说了,引入security后所有的方法需要经过登录验证才能访问,而我们的登录接口和验证码是不要登录验证的,因此需要在配置文件中放行这两个接口

新建一个SecurityConfig配置类,这个配置文件后面会详细

@EnableWebSecurity
public class SecurityConfig {
    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
                .authorizeHttpRequests(auhtor-> auhtor
                        //这里配置的接口不需要验证
                        .antMatchers("/admin/code","/admin/login").permitAll()
                        //其它接口都需要经过验证
                        .anyRequest().authenticated()

                );
        return http.build();
    }
}

"/admin/code","/admin/login"这两个请求被允许通过,anyRequest()其它任何请求必须经过验证

4.3自定义登录过滤器

接下来就是重头戏了,如何仿照源码做开发了,按照我们源码分析的流程,创建一个名为AdminUsernamePasswordAuthenticationFilter过滤器,找到UsernamePasswordAuthenticationFilter,直接复制源码,然后进行修改,以下是贴出核心代码:

public static final String SPRING_SECURITY_FORM_USERNAME_KEY = "username";
public static final String SPRING_SECURITY_FORM_PASSWORD_KEY = "password";
//(1)新增验证码
public static final String SPRING_SECURITY_FORM_CODE_KEY  = "code";
//(2)改成我们登录地址
private static final AntPathRequestMatcher DEFAULT_ANT_PATH_REQUEST_MATCHER = new AntPathRequestMatcher("/admin/login", "POST");

private String usernameParameter = SPRING_SECURITY_FORM_USERNAME_KEY;

private String passwordParameter = SPRING_SECURITY_FORM_PASSWORD_KEY;

private String codeParameter = SPRING_SECURITY_FORM_CODE_KEY;

private boolean postOnly = true;

public AdminUsernamePasswordAuthenticationFilter() {
    super(DEFAULT_ANT_PATH_REQUEST_MATCHER);
}

public AdminUsernamePasswordAuthenticationFilter(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());
    }
    //(3)验证码判断
    String code = obtainCode(request);
    Jedis jedis = RedisDS.create().getJedis();
    if (!jedis.exists(RedisKey.ADMIN_VERIFY_CODE+code)){
        System.out.print("Verification Code is error");
        throw new AuthenticationServiceException("Verification Code is error");
    }else{
        //删除缓存
        jedis.del(RedisKey.ADMIN_VERIFY_CODE+code);
    }

    String username = obtainUsername(request);
    username = (username != null) ? username.trim() : "";
    String password = obtainPassword(request);
    password = (password != null) ? password : "";
    UsernamePasswordAuthenticationToken authRequest = UsernamePasswordAuthenticationToken.unauthenticated(username,
                                                                                                          password);
    setDetails(request, authRequest);

    return this.getAuthenticationManager().authenticate(authRequest);
}

这里只是加了一个图片验证码的校验。

当然这里还有另外一种方式,因为UsernamePasswordAuthenticationFilter本身就提供用户名密码验证的功能,我们可以使用它,至于验证码就定义一个过滤器进行校验。

另外还有一点,这里并没有对UsernamePasswordAuthenticationToken进行重新定义,因为这个类本身就基于用户名和密码的,如果自定义,后面会遇到一些坑,你可以回顾一下ProviderManager源码,里面就有提高到过:

if (!provider.supports(toTest)) {
    //如果provider不支持UsernamePasswordAuthenticationToken,就跳到下一个provider
    continue;
}

到这里初步的登录验证已经完成,接下来就是如何将自己的过滤器注入到sprngsecurity的过滤链中

4.4添加到springsecurity过滤链

上面我们已经定义了一个AdminUsernamePasswordAuthenticationFilter过滤器,必须把它添加到容器中,它才能生效

新建一个名为SpringSecurityAdminConfig的配置文件,用来管理后台用户名密码登录,其内容如下

@Component
public class SpringSecurityAdminConfig extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {
    @Override
    public void configure(HttpSecurity http) throws Exception {
        AdminUsernamePasswordAuthenticationFilter adminUsernamePasswordAuthenticationFilter = new AdminUsernamePasswordAuthenticationFilter();
        adminUsernamePasswordAuthenticationFilter.setAuthenticationManager(http.getSharedObject(AuthenticationManager.class));

        //注入过滤器,addFilterAt替换UsernamePasswordAuthenticationFilter
        http.addFilterAt(adminUsernamePasswordAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
    }
}

注意我们是用adminUsernamePasswordAuthenticationFilter替换了原来的UsernamePasswordAuthenticationFilter

然后在SecurityConfig中加载SpringSecurityAdminConfig即可

@EnableWebSecurity
public class SecurityConfig {
    @Resource
    SpringSecurityAdminConfig springSecurityAdminConfig;

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
            	//禁用它,因为前后端分离不需要
            	.csrf(AbstractHttpConfigurer::disable)
            	//禁用session
            	.sessionManagement(AbstractHttpConfigurer::disable)
                .authorizeHttpRequests(auhtor-> auhtor
                        .antMatchers("/admin/code","/admin/login").permitAll()
                        .anyRequest().authenticated()

                );

        //后台登录配置
        http.apply(springSecurityAdminConfig);
        return http.build();
    }
}

http.apply就说引入其它配置,包括后面的手机号登录也是这样,这样比较方便管理。

另外还禁用了csrf和session,因为前后端分离的项目不需要这两个东西

//禁用它,因为前后端分离不需要
.csrf(AbstractHttpConfigurer::disable)
//禁用session
.sessionManagement(AbstractHttpConfigurer::disable)

如何知道它是否添加到容器中?

在"2.2springSecurity的本质"有提到,每次启动时,控制台都会打印出存在容器中的过滤器,是否添加成功,只要看一下就知道了。

4.5从数据库查询用户信息

虽然我们自定义了登录过滤器,但并没有改变用户信息来源于内存,而不是数据库。在阅读源码的时候我们已经说过,我们要重写UserDetailsService接口里面的loadUserByUsername方法。

新建BaseUserDetailsService类,继承UserDetailsService,内容如下:

package com.example.springsecuritydemo1.springsecurity.service;

import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;

/**
 * @author mayanhui
 */
public interface BaseUserDetailsService extends UserDetailsService {

    /**
     *手机号登录
     */
    UserDetails loadUserByPhone(String phone) throws UsernameNotFoundException;

    /**
     *微信
     */
    //UserDetails loadUserByAppId(String appId) throws UsernameNotFoundException;

}

这里我们扩展了通过手机号查询用户信息的方法,这个我们后面会用到。

接下来就是新建UserDetailServiceImpl类,实现这两个方法

@Service
public class UserDetailServiceImpl implements BaseUserDetailsService {
	@Autowired
    SysUserMapper sysUserMapper;
    
    @Override
    public SysUserDTO loadUserByUsername(String username) throws UsernameNotFoundException {
        //通过用户名查询用户信息
        LambdaQueryWrapper<SysUser> wrapper = new LambdaQueryWrapper<>();
        wrapper.eq(SysUser::getUserName, username);
        List<SysUser> sysUsers = sysUserMapper.selectList(wrapper);
        if (CollectionUtils.isEmpty(sysUsers)){
            throw new ServiceException("该用户不存在");
        }

        //获取权限信息
        List<SysMenu> userHasMenu = sysUserMapper.getUserHasMenu(sysUsers.get(0).getId());
        
        return new SysUserDTO(sysUsers.get(0),userHasMenu);
    }

    @Override
    public SysUserDTO loadUserByPhone(String phone) throws UsernameNotFoundException {
        return null;
    }
}

这些接口的返回值类型都是UserDetails,这里的SysUserDTO是继承UserDetails,主要是保存用户信息及权限信息,可以参考一下其内置的User类,然后做出一下的修改

@Data
//(1)必须继承UserDetails
public class SysUserDTO implements UserDetails {
    //(2)把用户信息封装成实体类,比较容易管理和操作,比如说新增一些字段,只需在实体类里面加上即可
    private SysUser sysUser;
	
    //(3)权限信息,这里需要注意的是要禁止序列化,不然存储到缓存中会有问题
    @JSONField(serialize = false)
    private List<GrantedAuthority> authorities;

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return this.authorities;
    }

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

    @Override
    public String getUsername() {
        return this.getUsername();
    }
	
    @Override
    public boolean isAccountNonExpired() {
        //账号是否过期,因为用户表里面没有这个字段,因此默认账号不过期,下面几个方法同理
        return true;
    }

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

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

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

到这里就已经写好了从数据库中查询用户信息的代码,接下来就是怎样使springsecurity使用我们写的loadUserByUsername这个,而不是其默认的。

springsecurity为此提供了使用配置的方式:

@EnableWebSecurity
public class SecurityConfig {
    @Autowired
    SpringSecurityAdminConfig springSecurityAdminConfig;

    @Autowired
    UserDetailServiceImpl userDetailService;

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
                .csrf(AbstractHttpConfigurer::disable)
                .sessionManagement(AbstractHttpConfigurer::disable)
                .authorizeHttpRequests(auhtor-> auhtor
                        .antMatchers("/admin/code", "/admin/login").permitAll()
                        .anyRequest().authenticated())
                .cors();
        //后台登录配置
        http.apply(springSecurityAdminConfig);
        //注入新的AuthenticationManager
        http.authenticationManager(authenticationManager(http));

        return http.build();
    }

    /**
     *密码加密规则
     */
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder(12);
    }

    /**
     *构造一个AuthenticationManager,使用自定义的userDetailsService和passwordEncoder
     */
    @Bean
    AuthenticationManager authenticationManager(HttpSecurity http) throws Exception {
        AuthenticationManager authenticationManager = http.getSharedObject(AuthenticationManagerBuilder.class)
                .userDetailsService(userDetailService)
                .passwordEncoder(passwordEncoder())
                .and()
                .build();
        return authenticationManager;
    }

}

首先我们是使用AuthenticationManagerBuilder这个构建器重新构造了一个AuthenticationManager,然后绑定我们写的userDetailService。

还记得创建表的时候插入了这样的一条用户数据

INSERT INTO `springsecurity`.`sys_user`(`user_name`, `nick_name`, `password`, `phone`, `status`) VALUES ('test', '1', '$2a$12$pgFnH5Ot.XIvbaTM7X9nNe8AGwBV.3eggszusKShXXG2HJ1fFdNMO', '13662301000', '0');

其密码是预先经过BCryptPasswordEncoder加密的,因此在做密码校验的时候也要使用它,而不是使用springSecurity默认的。

最后就是替换原来的

//注入新的AuthenticationManager
http.authenticationManager(authenticationManager(http));

接下来就登录测试一下:

not found "/" 说明已经登录成功了,还记得开头那会,登录成功后会跳转到首页(/),但很显然这并不满足前后的分离项目,因此还需要进行改造。

4.6登录成功/失败处理器

前后端分离的项目是通过header携带token进行验证的,因此登录成功后需要返回一窜token,我们在阅读AbstractAuthenticationProcessingFilter源码时已经讲过其默认是跳转到固定页面,因此需要我们需要自定义一个successHandler和failureHandler

新建AdminAuthenticationSuccessHandler,让它继承AuthenticationSuccessHandler

/**
 * @Description: 后台登录成功处理器
 * @Author: mayanhui
 * @Date: 2023/2/14 12:43
 */
@Component
public class AdminAuthenticationSuccessHandler implements AuthenticationSuccessHandler {

    @Override
    public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
        //拿到登录用户信息
        SysUserDTO userDetails = (SysUserDTO)authentication.getPrincipal();

        //生成jwt(token)
        Map<String, Object> map = new HashMap<>();
        map.put("uid", userDetails.getSysUser().getId());
        map.put("expire_time", System.currentTimeMillis() + 1000 * 60 * 60 * 24 * 15);
        String jwtToken = JWTUtil.createToken(map, "1234".getBytes());

        //将用户信息保存到redis
        Jedis jedis = RedisDS.create().getJedis();
        String key = RedisKey.ADMIN_USER_INFO + userDetails.getSysUser().getId().toString();
        jedis.set(key,JSON.toJSONString(userDetails));

        //当前token也保存到redis//单点登录
        jedis.set(RedisKey.ADMIN_USER_TOKEN + userDetails.getSysUser().getId().toString(),jwtToken);

        Map<String,Object> resultMap = new HashMap<>();
        resultMap.put("token", TokenHeader.ADMIN_TOKEN_PREFIX+jwtToken);

        //输出结果
        httpServletResponse.setCharacterEncoding("utf-8");
        httpServletResponse.setContentType("application/json");
        				      httpServletResponse.getWriter().write(JSON.toJSONString(ResultUtil.ok(resultMap)));
    }
}

注意在返回token的时候加了一个前缀,TokenHeader.ADMIN_TOKEN_PREFIX,其作用就说区分用户名密码登录还是其它登录方式生产的token。

接下来就是将它传入到AdminUsernamePasswordAuthenticationFilter,因为这里是这样的:

AdminUsernamePasswordAuthenticationFilter继承AbstractAuthenticationProcessingFilter,同时继承了相关属性

打开SpringSecurityAdminConfig,创建过滤器的时候,设置一下AuthenticationSuccessHandler即可

@Component
public class SpringSecurityAdminConfig extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {
    @Autowired
    AdminAuthenticationSuccessHandler adminAuthenticationSuccessHandler;

    @Override
    public void configure(HttpSecurity http) throws Exception {
        AdminUsernamePasswordAuthenticationFilter adminUsernamePasswordAuthenticationFilter = new AdminUsernamePasswordAuthenticationFilter();
        adminUsernamePasswordAuthenticationFilter.setAuthenticationManager(http.getSharedObject(AuthenticationManager.class));
        
   adminUsernamePasswordAuthenticationFilter.setAuthenticationSuccessHandler(adminAuthenticationSuccessHandler);//传入我们自定义的成功的处理器

        //注入过滤器,addFilterAt替换UsernamePasswordAuthenticationFilter
        http.addFilterAt(adminUsernamePasswordAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
    }

同理,登录失败的处理器也是这个逻辑

/**
 * @Description: 后台登录失败处理器
 * @Author: mayanhui
 * @Date: 2023/2/14 12:43
 */
@Component
public class AdminAuthenticationFailureHandler implements AuthenticationFailureHandler {

    @Override
    public void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
        //修改编码格式
        httpServletResponse.setCharacterEncoding("utf-8");
        httpServletResponse.setContentType("application/json");

        if (e instanceof BadCredentialsException){
            httpServletResponse.getWriter().write(JSON.toJSONString(ResultUtil.fail(1000,"用户名或密码错误")));
        }else {
            httpServletResponse.getWriter().write(JSON.toJSONString(ResultUtil.fail(1000,e.getMessage())));
        }

    }
}

启动测试一下:

4.7认证过滤器

当我们登录成功后,是通过token去访问我们接口的,因此需要自定义一个过滤器,这个过滤器会去获取请求头中的token,对token进行解析取出其中的用户ID,然后根据用户ID去获取缓存中的用户信息,存入到SecurityContextHolder中

@Component
@Slf4j
public class AdminAuthenticationTokenFilter extends OncePerRequestFilter {

    @Override
    protected void doFilterInternal(HttpServletRequest request,
                                    HttpServletResponse response,
                                    FilterChain chain) throws ServletException, IOException {

        //获取token
        String token = request.getHeader("token");
        if (!StringUtils.hasLength(token) || !token.startsWith(TokenHeader.ADMIN_TOKEN_PREFIX)){
            //如果不存在和前缀不是TokenHeader.ADMIN_TOKEN_PREFIX,就放行到下一个过滤器
            chain.doFilter(request, response);
            SecurityContextHolder.clearContext();
            return;
        }
        //获取真实的token(去掉前缀)
        String authToken = token.substring(TokenHeader.ADMIN_TOKEN_PREFIX.length());

        //解析token
        JWT jwt;
        String code = null;
        try{
            jwt = JWTUtil.parseToken(authToken);
        }catch (Exception e){
            chain.doFilter(request, response);
            return;
        }
        if (!Objects.isNull(jwt.getPayload("uid"))) {
            code = jwt.getPayload("uid").toString();
        }
        if (!StringUtils.hasLength(code)){
            chain.doFilter(request, response);
            return;
        }

        Jedis jedis = RedisDS.create().getJedis();
        //单点登录
        if (!authToken.equals(jedis.get(RedisKey.ADMIN_USER_TOKEN+code))){
            chain.doFilter(request, response);
            return;
        }

        //从缓存中获取用户信息
        String key = RedisKey.ADMIN_USER_INFO + code;
        if (!jedis.exists(key)){
            chain.doFilter(request, response);
            return;
        }

        Object userObj = jedis.get(key);
        if (Objects.isNull(userObj)){
            chain.doFilter(request, response);
            return;
        }

        //保存相关的验证信息
        SysUserDTO user = JSONObject.parseObject(userObj.toString(), SysUserDTO.class);
        UsernamePasswordAuthenticationToken authenticationToken =
                new UsernamePasswordAuthenticationToken(user, null, user.getAuthorities());

        SecurityContextHolder.getContext().setAuthentication(authenticationToken);
        chain.doFilter(request, response);
    }
}

这里我们继承OncePerRequestFilter,看名字大概猜出它的意思:“同一个请求只处理一次”。就类似于SecurityContextPersistenceFilter源码中的

//(1)同一个请求只处理一次
if (request.getAttribute(FILTER_APPLIED) != null) {
    chain.doFilter(request, response);
    return;
}

另外在保存用户信息时 ,要创建一个token

UsernamePasswordAuthenticationToken authenticationToken =
                new UsernamePasswordAuthenticationToken(user, null, user.getAuthorities());

然后把过滤器注入到springsecurity的过滤链中,打开SpringSecurityAdminConfig

//注入过滤器,addFilterAt替换UsernamePasswordAuthenticationFilter
http.addFilterAt(adminUsernamePasswordAuthenticationFilter, UsernamePasswordAuthenticationFilter.class)
 .addFilterBefore(adminAuthenticationTokenFilter,AdminUsernamePasswordAuthenticationFilter.class);

把它加在AdminUsernamePasswordAuthenticationFilter这个过滤器前面。

4.8认证异常抛出

如果未登录,或者登录后,随便携带一个token,它是如何抛出异常的?

springsecurity中的HttpSecurity对象提供了了exceptionHandling方法,只需要在配置文件中绑定相关的处理接口即可

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    http
        .csrf(AbstractHttpConfigurer::disable)
        .sessionManagement(AbstractHttpConfigurer::disable)
        .authorizeHttpRequests(auhtor-> auhtor
                               .antMatchers("/admin/code", "/admin/login").permitAll()
                               .anyRequest().authenticated())
        .cors();
    //后台登录配置
    http.apply(springSecurityAdminConfig);
    //注入新的AuthenticationManager
    http.authenticationManager(authenticationManager(http));
	
    //认证异常处理器
    http.exceptionHandling(ex->ex.authenticationEntryPoint(authenticationEntryPointIHandler));

    return http.build();
}

authenticationEntryPointIHandler内容如下:

@Component
public class AuthenticationEntryPointIHandler implements AuthenticationEntryPoint {

    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
        response.setCharacterEncoding("UTF-8");
        response.setContentType("application/json");
        response.getWriter().println(JSONUtil.toJsonStr(ResultUtil.fail(HttpStatus.UNAUTHORIZED.value(), "认证失败,请重新登录")));
        response.getWriter().flush();
    }
}

效果如下:

4.9退出登录

springsecurity中的HttpSecurity对象提供了logout方法,但本教程使用自定义的方式,写一个退出登录的接口

@GetMapping("/admin/logout")
public ResultUtil<Void> adminLogout(){
    String key = RedisKey.ADMIN_USER_INFO + httpRequestComponent.getAdminUserId().toString();
    //删除缓存即可
    Jedis jedis = RedisDS.create().getJedis();
    jedis.del(key);
    SecurityContextHolder.clearContext();
    return ResultUtil.ok();
}

这段代码还是比较简单的,主要是删除缓存及清空SecurityContextHolder里面的用户信息,httpRequestComponent是获取登录用户信息的组件,代码如下:

@Component
public class HttpRequestComponent {

    /**
     * 获取token
     */
    public String getToken(){
        HttpServletRequest request = ((ServletRequestAttributes) Objects.requireNonNull(RequestContextHolder.getRequestAttributes())).getRequest();
        String token = request.getHeader("token");
        if (StringUtils.isEmpty(token)) {
            throw new ServiceException("授权令牌为空");
        }
        return token;
    }

    /**
     * 获取用户信息
     */
    public SysUserDTO getAdminUserInfo(){
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        SysUserDTO userDetialsDTO = (SysUserDTO) authentication.getPrincipal();
        if (Objects.isNull(userDetialsDTO)){
            throw new ServiceException(10000,"登录失效,请重新登录");
        }

        return userDetialsDTO;
    }

    /**
     * 获取用户ID
     */
    public Long getAdminUserId(){
        if (Objects.isNull(this.getAdminUserInfo().getSysUser())){
            throw new ServiceException(10000,"登录失效,请重新登录");
        }
        return this.getAdminUserInfo().getSysUser().getId();
    }
}

以上就是用户名密码登录验证的整个过程,接下来就是手机号登录验证

5.手机号登录

手机号登录的流程跟用户名密码原理是一样的,因此改造起来就比较简单了,还是跟用户名密码登录一样,咱们照猫画虎。

5.1获取手机验证码

@GetMapping("/mobile/code")
public String getMobileCode(String mobile){
    //产生四位随机数
    long rand = RandomUtil.randomLong(1000, 9999);
    //调用手机服务商接口
    
    Jedis jedis = RedisDS.create().getJedis();
    jedis.set(RedisKey.ADMIN_VERIFY_CODE +mobile,String.valueOf(rand));

    return String.valueOf(rand);
}

获取手机验证码,这里做了模拟,通过接口返回。

注:验证码接口要放行

.antMatchers("/admin/code", "/admin/login","/mobile/code").permitAll()

5.2自定义登录过滤器

新建一个MobileAuthenticationFilter过滤器

以下是核心代码:

public class MobileAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
    //手机号
    public static final String SPRING_SECURITY_MOBILE_KEY = "mobile";
    //验证码
    public static final String SPRING_SECURITY_CODE_KEY  = "code";

    //改成手机号登录地址
    private static final AntPathRequestMatcher DEFAULT_ANT_PATH_REQUEST_MATCHER = new AntPathRequestMatcher("/mobile/login", "POST");

    private String mobileParameter = SPRING_SECURITY_MOBILE_KEY;

    private String codeParameter = SPRING_SECURITY_CODE_KEY;

    private boolean postOnly = true;

    public MobileAuthenticationFilter() {
        super(DEFAULT_ANT_PATH_REQUEST_MATCHER);
    }

    public MobileAuthenticationFilter(AuthenticationManager authenticationManager) {
        super(DEFAULT_ANT_PATH_REQUEST_MATCHER, authenticationManager);
    }

    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
            throws AuthenticationException {
        if (postOnly && !request.getMethod().equals(HttpMethod.POST.name())) {
            throw new AuthenticationServiceException(
                    "Authentication method not supported: " + request.getMethod());
        }
        String mobile = obtainMobile(request);
        if (mobile == null) {
            mobile = "";
        }
        mobile = mobile.trim();
		
        if (!StringUtils.hasLength(code)){
            throw new AuthenticationServiceException("Verification Code is empty");
        }
        //判断输入的验证码是否正确
        String code = obtainCode(request);
        Jedis jedis = RedisDS.create().getJedis();
        if (!code.equals(jedis.get(RedisKey.MOBILE_VERIFY_CODE+mobile))){
            throw new AuthenticationServiceException("Verification Code is error");
        }

        //生产一个手机号验证令牌
        MobileAuthenticationToken mobileAuthenticationToken = new MobileAuthenticationToken(mobile);
        setDetails(request, mobileAuthenticationToken);

        return this.getAuthenticationManager().authenticate(mobileAuthenticationToken);
    }

	…………………………………………
}

需要注意的是跟用户名密码验证不同的是身份认证令牌MobileAuthenticationToken,需要自定义,其内容如下:

public class MobileAuthenticationToken extends AbstractAuthenticationToken {
    private final Object principal;

    public MobileAuthenticationToken(String mobile){
        super(null);
        this.principal = mobile;
        super.setAuthenticated(false);
    }

    public MobileAuthenticationToken(Object principal, Collection<? extends GrantedAuthority> authorities) {
        super(authorities);
        this.principal = principal;
        super.setAuthenticated(true);
    }

    @Override
    public Object getCredentials() {
        return null;
    }

    @Override
    public Object getPrincipal() {
        return this.principal;
    }

    @Override
    public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException {
        if (isAuthenticated) {
            throw new IllegalArgumentException(
                    "Cannot set this token to trusted - use constructor which takes a GrantedAuthority list instead");
        }

        super.setAuthenticated(false);
    }

    @Override
    public void eraseCredentials() {
        super.eraseCredentials();
    }

}

以上内容比较简单,抄过来改动一下即可

5.3添加到springsecurity过滤链

新建一个名为SpringSecurityMobileConfig的配置文件,用来管理后手机号登录登录,其内容如下

@Component
public class SpringSecurityMobileConfig extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {

    @Override
    public void configure(HttpSecurity http) throws Exception {
        MobileAuthenticationFilter mobileAuthenticationFilter = new MobileAuthenticationFilter();
        mobileAuthenticationFilter.setAuthenticationManager(http.getSharedObject(AuthenticationManager.class));

        //注入过滤器
        http.addFilterAfter(mobileAuthenticationFilter,AdminUsernamePasswordAuthenticationFilter.class);
    }


}

然后在SecurityConfig中调用该配置

//后台登录配置
http.apply(springSecurityAdminConfig);
//注入新的AuthenticationManager
http.authenticationManager(authenticationManager(http));

//手机登录配置
http.apply(springSecurityMobileConfig);

最后启动一下容器,看看控制台是否打印出MobileAuthenticationFilter

5.4从数据库查询用户信息

这里跟用户密码登录有点区别,回顾一下,在源码部分我们讲到了ProviderManager这个管理类,其中一个DaoAuthenticationProvider,它的作用就是对用户名进行验证。

而springsecurity中没有提供对手机号进行验证的,因此这里我们需要自定义一个Provider。

public class MobileAuthenticationProvider implements AuthenticationProvider {

    private UserDetailServiceImpl userDetailService;

    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        MobileAuthenticationToken mobileAuthenticationToken = (MobileAuthenticationToken) authentication;
		//(1)通过手机号从数据库中查询用户信息
        SysUserDTO sysUserDTO = this.userDetailService.loadUserByPhone(mobileAuthenticationToken.getPrincipal().toString());

        if (Objects.isNull(sysUserDTO)){
            throw new BadCredentialsException("手机登录失败");
        }

        MobileAuthenticationToken authenticationToken = new MobileAuthenticationToken(sysUserDTO,sysUserDTO.getAuthorities());
        authenticationToken.setDetails(authenticationToken.getCredentials());
		
        //返回一个验证token
        return authenticationToken;
    }

    @Override
    public boolean supports(Class<?> authentication) {
        return MobileAuthenticationToken.class.isAssignableFrom(authentication);
    }

    public void setBaseUserDetailsService(UserDetailServiceImpl userDetailsService){
        this.userDetailService = userDetailsService;
    }
}

接下来实现我们前面定义好的loadUserByPhone

@Override
public SysUserDTO loadUserByPhone(String phone) throws UsernameNotFoundException {
    //用户信息
    LambdaQueryWrapper<SysUser> wrapper = new LambdaQueryWrapper<>();
    wrapper.eq(SysUser::getPhone, phone);
    List<SysUser> sysUsers = sysUserMapper.selectList(wrapper);
    if (CollectionUtils.isEmpty(sysUsers)){
        throw new ServiceException("该用户不存在");
    }
    //不做授权,把authorities设置为空

    return new SysUserDTO(sysUsers.get(0),null);
}

这里面的逻辑也很简单,参考loadUserByUsername即可。

把自定义的MobileAuthenticationProvider加入容器中

@Override
public void configure(HttpSecurity http) throws Exception {
    MobileAuthenticationFilter mobileAuthenticationFilter = new MobileAuthenticationFilter();
    mobileAuthenticationFilter.setAuthenticationManager(http.getSharedObject(AuthenticationManager.class));

    //new一个mobileAuthenticationProvider
    MobileAuthenticationProvider mobileAuthenticationProvider = new MobileAuthenticationProvider();
    mobileAuthenticationProvider.setBaseUserDetailsService(userDetailService);

    //注入过滤器
    http.authenticationProvider(mobileAuthenticationProvider)
        .addFilterAfter(mobileAuthenticationFilter,AdminUsernamePasswordAuthenticationFilter.class);
}

5.5登录成功/失败处理器

整个逻辑和用户名密码登录是一样的,只是改了一些KEY值,这里就不重复讲

MobileAuthenticationSuccessHandler

@Component
public class MobileAuthenticationSuccessHandler implements AuthenticationSuccessHandler {

    @Override
    public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
        //拿到登录用户信息
        SysUserDTO userDetails = (SysUserDTO)authentication.getPrincipal();

        //生成jwt
        Map<String, Object> map = new HashMap<>();
        map.put("uid", userDetails.getSysUser().getId());
        map.put("expire_time", System.currentTimeMillis() + 1000 * 60 * 60 * 24 * 15);
        String jwtToken = JWTUtil.createToken(map, "1234".getBytes());

        //将用户信息保存到redis
        Jedis jedis = RedisDS.create().getJedis();
        String key = RedisKey.MOBILE_USER_INFO + userDetails.getSysUser().getId().toString();
        jedis.set(key,JSON.toJSONString(userDetails));

        //当前token也保存到redis//单点登录
        jedis.set(RedisKey.MOBILE_USER_TOKEN + userDetails.getSysUser().getId().toString(),jwtToken);

        Map<String,Object> resultMap = new HashMap<>();
        resultMap.put("token", TokenHeader.MOBILE_TOKEN_PREFIX+jwtToken);

        //输出结果
        httpServletResponse.setCharacterEncoding("utf-8");
        httpServletResponse.setContentType("application/json");
        httpServletResponse.getWriter().write(JSON.toJSONString(ResultUtil.ok(resultMap)));
    }
}

MobileAuthenticationFailureHandler

@Component
public class MobileAuthenticationFailureHandler implements AuthenticationFailureHandler {

    @Override
    public void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
        //修改编码格式
        httpServletResponse.setCharacterEncoding("utf-8");
        httpServletResponse.setContentType("application/json");

        if (e instanceof BadCredentialsException){
            httpServletResponse.getWriter().write(JSON.toJSONString(ResultUtil.fail(1000,"手机号登录异常")));
        }else {
            httpServletResponse.getWriter().write(JSON.toJSONString(ResultUtil.fail(1000,e.getMessage())));
        }
    }
}

SpringSecurityMobileConfig

@Component
public class SpringSecurityMobileConfig extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {

    @Autowired
    UserDetailServiceImpl userDetailService;

    @Autowired
    MobileAuthenticationSuccessHandler mobileAuthenticationSuccessHandler;

    @Autowired
    MobileAuthenticationFailureHandler mobileAuthenticationFailureHandler;

    @Override
    public void configure(HttpSecurity http) throws Exception {
        MobileAuthenticationFilter mobileAuthenticationFilter = new MobileAuthenticationFilter();
        mobileAuthenticationFilter.setAuthenticationManager(http.getSharedObject(AuthenticationManager.class));
        mobileAuthenticationFilter.setAuthenticationSuccessHandler(mobileAuthenticationSuccessHandler);
        mobileAuthenticationFilter.setAuthenticationFailureHandler(mobileAuthenticationFailureHandler);

        //new一个mobileAuthenticationProvider
        MobileAuthenticationProvider mobileAuthenticationProvider = new MobileAuthenticationProvider();
        mobileAuthenticationProvider.setBaseUserDetailsService(userDetailService);

        //注入过滤器
        http.authenticationProvider(mobileAuthenticationProvider)
                .addFilterAfter(mobileAuthenticationFilter,AdminUsernamePasswordAuthenticationFilter.class);
    }
    
}

5.6认证过滤器

整个逻辑和用户名密码登录是一样的,只是改了一些KEY值,这里就不重复讲

@Component
@Slf4j
public class MobileAuthenticationTokenFilter extends OncePerRequestFilter {

    @Override
    protected void doFilterInternal(HttpServletRequest request,
                                    HttpServletResponse response,
                                    FilterChain chain) throws ServletException, IOException {

        //获取token
        String token = request.getHeader("token");
        if (!StringUtils.hasLength(token) || !token.startsWith(TokenHeader.MOBILE_TOKEN_PREFIX)){
            //如果不存在和前缀不是TokenHeader.MOBILE_TOKEN_PREFIX,就放行到下一个过滤器
            chain.doFilter(request, response);
            SecurityContextHolder.clearContext();
            return;
        }
        //获取真实的token(去掉前缀)
        String authToken = token.substring(TokenHeader.MOBILE_TOKEN_PREFIX.length());

        //解析token
        JWT jwt;
        String code = null;
        try{
            jwt = JWTUtil.parseToken(authToken);
        }catch (Exception e){
            chain.doFilter(request, response);
            return;
        }
        if (!Objects.isNull(jwt.getPayload("uid"))) {
            code = jwt.getPayload("uid").toString();
        }
        if (!StringUtils.hasLength(code)){
            chain.doFilter(request, response);
            return;
        }

        Jedis jedis = RedisDS.create().getJedis();
        //单点登录
        if (!authToken.equals(jedis.get(RedisKey.MOBILE_USER_TOKEN+code))){
            chain.doFilter(request, response);
            return;
        }

        //从缓存中获取用户信息
        String key = RedisKey.MOBILE_USER_INFO + code;
        if (!jedis.exists(key)){
            chain.doFilter(request, response);
            return;
        }

        Object userObj = jedis.get(key);
        if (Objects.isNull(userObj)){
            chain.doFilter(request, response);
            return;
        }

        //保存手机号验证的相关信息
        SysUserDTO user = JSONObject.parseObject(userObj.toString(), SysUserDTO.class);
        MobileAuthenticationToken authenticationToken = new MobileAuthenticationToken(user, null);

        SecurityContextHolder.getContext().setAuthentication(authenticationToken);
        chain.doFilter(request, response);
    }
}

5.7退出登录

@GetMapping("/mobile/logout")
public ResultUtil<Void> mobileLogout(){
    String key = RedisKey.MOBILE_USER_INFO + httpRequestComponent.getAdminUserId().toString();
    //删除缓存即可
    Jedis jedis = RedisDS.create().getJedis();
    jedis.del(key);
    SecurityContextHolder.clearContext();
    return ResultUtil.ok();
}

6.授权

6.1权限验证的流程

springsecurity中,会使用 AuthorizationFilter进行权限校验,如下图所示(官网文档截图)

① AuthorizationFilter 通过 SecurityContextHolder上下文对象去获取已经登录的用户信息

②AuthorizationFilter创建一个FilterInvocation,用于保存一些对象

③将前面的一些对象传入AuthorizationManager,进行验证

④失败………………

⑤成功……

因为授权部分不需要改造,就不看源码了,如果有兴趣大家可以根据这个图去阅读源码。

6.2授权信息的封装

我们在实现loadUserByUsername这个方法的时候有这样的一段代码:

//获取权限信息
List<GrantedAuthority> authorities = new ArrayList<>();
List<SysMenu> userHasMenu = sysUserMapper.getUserHasMenu(sysUsers.get(0).getId());
if (!CollectionUtils.isEmpty(userHasMenu)){
    for (SysMenu menu : userHasMenu){
        if (!StringUtils.hasLength(menu.getPerms())){
            SimpleGrantedAuthority simpleGrantedAuthority = new SimpleGrantedAuthority(menu.getPerms());
            authorities.add(simpleGrantedAuthority);
        }
    }
}
return new SysUserDTO(sysUsers.get(0),authorities);

在查询用户基本信息的同时也把权限信息给查出来,并且保存到缓存中。

上面我们知道了授权验证这个流程是springsecurity已经封装好,并且不需要改造它。因此接下来的问题就是,怎么给接下来的用户请求授权。

springsecurity提供了多种方式

注解方式:

在此之前需要在配置文件中开启授权验证的配置

@EnableGlobalMethodSecurity(prePostEnabled = true)
@GetMapping("/list")
@PreAuthorize("hasAuthority('sysUser/list')")
public ResultUtil<String> list(){
    return ResultUtil.ok(200,"访问列表成功");
}

基于配置的方式:

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    http
        .csrf(AbstractHttpConfigurer::disable)
        .sessionManagement(AbstractHttpConfigurer::disable)
        .authorizeHttpRequests(auhtor-> auhtor
                               .antMatchers("/verify/code","/admin/login").permitAll()
                          .antMatchers("/user/sysUser/list").hasAuthority("sysUser/list")
                               .anyRequest().authenticated())
        .addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class)
        .cors();

    return http.build();
}

到此为止整个登录验证及授权过程已经完成

6.3授权异常抛出

在认证异常抛出有这样的一段代码:

//认证异常处理器
http.exceptionHandling(ex->ex.authenticationEntryPoint(authenticationEntryPointIHandler));

它也提供了授权异常的方法:accessDeniedHandler,我们定义一个授权异常处理器,代码如下:

@Component
public class AccessDeniedImplHandler implements AccessDeniedHandler {

    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException {
        response.setCharacterEncoding("UTF-8");
        response.setContentType("application/json");
        response.getWriter().println(JSONUtil.toJsonStr(ResultUtil.fail(HttpStatus.FORBIDDEN.value(),"没有权限访问")));
        response.getWriter().flush();
    }
}

但是会发现并不生效

主要是因为我们定义了一个全局异常,基于注解的授权方式,抛出的异常会被全局异常捕获,因此在全局异常类中加入如下代码:

GlobalExceptionHandler

@ExceptionHandler(AccessDeniedException.class)
public ResultUtil<Void> accessDeniedException(AccessDeniedException e) throws AccessDeniedException {
    return ResultUtil.fail(403, "没有权限访问");
}

7.微信、qq登录

微信、qq登录等第三方登录也可以参考手机登录的模式,自定义自己的登录方式。

springSecurity也提供了OAuth 2.0登录功能,并且实现了用例"使用Google登录”或“使用GitHub登录"

这一块更新中……

源码:https://gitee.com/myha/springsecurity

SpringBoot--Spring Security(二) Spring Security 是基于 Spring 的用户认证(Authentication)和用户授权(Authorization)框架,提供了一套 Web应用安全性的完整解决方案。其中核心技术使用了 Servlet 过滤器、IOC 和 AOP 等。 阅读详情

相关推荐

(Spring Security)实战干货二:标准登录表单,除用户名密码外增添其他字段

在很多的登录场景中,用户名和密码不是仅有的用于登录认证授权的字段,会有各类的情况。比如:学校系统登录时需要加选学生或老师身份,多组织系统登录时需要选择组织(域)等等。这篇文章描述用SS实现这类场景登录。

希克的博客 865

Spring Boot 2.7.0 新手入门(一)

spring boot 2.7.0入门教程

qq_36378416的博客 1万+

springboot2 + activiti7 整合

springboot2 + activiti7 整合activiti7默认集成了SpringSecurity,坏处是显而易见的,那就是在微服务情况下,我们的很多服务是不集成SpringSecurity,而且activiti所在的springboot也不会是和统一认证的springboot放在一起,违背了微服务划分的原则。这里我们在后续的地方讲解是如何利用框架的这段代码解决的springsecurity权限问题。

u010291020的博客 803

重磅!Spring Boot 2.7 正式发布,一大波新特性,看完我彻底躺平了。。

点击关注公众号,Java干货及时送达Spring Boot 2.7 来了大家好,我是栈长。Spring Boot 2.6.0 发布已经过去大半年了,现在 Spring Boot 2.7.0 如期而至:Spring Boot 又接连发布了三个版本:Spring Boot 2.7.0(最新)Spring Boot 2.6.8Spring Boot 2.5.14后面两个版本都是...

Java技术栈,分享最主流的Java技术 1万+

A robust and easy to implement method for IMU calibration without external equipments(IMU标定)

A robust and easy to implement method for IMU calibration without external equipments 1 标定原理1 亲试可用,还有动态演示效果不错。相比商用产品的简易标定流程,该标定方法有以下几点不同: 给出了加速度计和陀螺仪更复杂的模型。 利用Allan方差的定义,标定陀螺仪的偏置。 大量冗余数据,保证最优化收敛。利用数十...

擦擦擦大侠的博客 3941

STM32Cubemx+CanFestival:从零到一的CANopen移植实战

本文详细介绍了如何使用STM32CubeMX和CanFestival实现CANopen协议栈的移植,从开发环境搭建到核心配置步骤,再到调试与问题排查。通过实战经验分享,帮助开发者快速掌握STM32与CANopen的结合应用,提升嵌入式系统开发效率。

weixin_42558093的博客 407

SpringBoot2.1.7整合SpringSecurity系列(一)

一 前言 安全管理框架中,Shiro和SpringSecurity都是响当当的存在。 相对于 Shiro,在 SSM/SSH 中整合 Spring Security 都是比较麻烦的操作,所以,Spring Security 虽然功能比 Shiro 强大,但是使用反而没有 Shiro 多(Shiro 虽然功能没有 Spring Security 多,但是对于大部分项目而言,Shiro 也够用了)。 ...

Terry‘s Blog 1614

C#串口通信从入门到精通(11)——串口接收的数据存储到本地文件(txt、csv)

本文详细介绍如何将串口通信将数据进行本次存储,主要介绍常用的csv、txt格式文件,由多年实战经验总结,绝对干货!

qq_34059233的博客 7230

SpringBoot2.7.X整合SpringSecurity+JWT(入门级简单易懂)

SpringBoot2.7.9整合SpringSecurity+JWT、入门级简单易懂

zyyxiaoxiao的博客 6551

C#上位机多PLC多协议开发思路:10年老工控师拆解实战方案,避坑90%的问题(附代码框架)

是否连接成功数据地址(如“DB1.DBW0”“D100”“4x0001”)

这里只有干货:从 Modbus 通信到 MES 对接,从 YOLO 训练到工控机部署,带你搞定工业软件开发全流程。 941

ISP算法:宽动态HDR

动态范围(Dynamic Range):在很多领域来表示某个变量的最大值和最小值的比值。在数字图像处理领域里,动态范围也被称为图像对比度,表示图像可以显示范围内,像素最大亮度值和最小亮度值的比值。 高动态范围图像(High Dynamic Range Image): 可以反映真实场景中高动态范围亮度信息图像。 高动态技术:能自动调整图像动态范围的一种技术。该技术是一种利用计算机高速运算产生高

a200800170331的专栏 1万+

SpringBoot2.7.4整合SpringSecurity

id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键id',`rolename` varchar(255) DEFAULT NULL COMMENT '角色,逗号隔开',`username` varchar(255) NOT NULL COMMENT '用户名',`password` varchar(255) NOT NULL COMMENT '密码',③.模拟当登录用户改成admin,再次访问获取所有用户接口时候的情况。component层。

qq_35160479的博客 918

基于Xilinx K7-410T的高速DAC之AD9129开发笔记(一)

引言:从本文开始,我们介绍下项目中设计的并行LVDS高速DAC接口设计,包括DAC与FPGA硬件接口设计、软件设计等。项目设计高速DAC采用了ADI公司的AD9129,该芯片最大更新速率5.7Gsps,该芯片在宽带通信应用、LTE、雷达信号产生、干扰机等领域有广泛应用。

FPGA_Designer 2531

springboot2.7.6与springsecurity和JWT整合方案

新版本的springsecurity与旧版本的使用方法有点不一样,最近探索了一下springsecurity与JWT的整合方案,进行一下总结。

woshihedayu的博客 1190

地图自定义编码CSV转换成ini文件

创 建 schema.ini 文件:有时,当您尝试使用 .csv 文件将自定义地理信息导入 Tableau 时,您可能会看到“无法使用.csv,因为它不包含唯一列”错误消息。这是因为您的 .csv 文件可能包含数值数据,例如数值邮政编码。对于新的地理角色,Tableau 将只接受文本字段。但是,您可以创建一个 schema.ini 文件,以指示 Tableau 应将您要导入的数值字段视为文本字段。...

wenyusuran的专栏 1970

华为OD机试 - 报文响应时间 - 贪心算法、位运算(Python/JS/C/C++ 2024 E卷 200分)

贪心算法是在每一步选择中都做出当前最优的选择,期望通过局部最优解推导出全局最优解的算法。

学Java,找哪吒 1756

springboot2.7.9 整合springSecurity+JWT+mybatis+Swagger 3.0+generator 3.5.2资料记录

只记录,后续整理

ErnestW的博客 638

最新的sdio 4.0 spec 协议标准 带书签 高清完整PDF版

Part-E1-SDIO-Specification-Ver4.00-Final-120220.pdf:最新的sdio 4.0 spec 协议标准 带书签 高清完整PDF版,包括SDIO 接口的外设,SDIO 接口官方协议等。

springboot2.7.6整合springsecurity相关配置

这里面的逻辑是,获取当前已认证的用户信息,保存在securityUser对象里面。其中,filterChain方法里面定义了最核心的配置信息,包括登录接口的请求路径,用户名、密码的参数名,登录成功的处理类的对象,登录失败的处理类对象,退出的接口路径,退出时删除cookie,退出的处理类对象,还包括处理跨域的配置等。这里面的逻辑就是,根据用户名查询用户信息,如果结果为空,就抛异常,如果不为空,就把用户信息保存到securityUser里面,并返回这个对象。登录的时候,会调用这个方法。2、这是核心配置方法。

woshihedayu的博客 2246

大中型超市收银系统(可直接运行)

光步前台收银系统系统功能:商品销售、商品退货、挂单、收银员权限、会员销售、单品取消、整单取消、商品查询、脱机销售、脱机数据上传,基本资料下载,开钱箱、小票打印、收款结算,支持多币种付款方式(现金、储值卡、礼券、微信等),支持多种促销。运行环境:.net 4.0登陆信息用户名:1001 密码:123456内置商品信息,商品编码可输入00001000 - 00010000,测试效果相关功能键F5 结算 F9挂单 F11商品查询 F8退货 Insert单品取消 Del整单取消 F12开钱箱F2 会员 Ctrl+Z退出系统 Home 锁屏 相关源码待公开

文档:亲测可用解决vnc灰屏,黑屏,鼠标大黑叉情况

主要解决vnc-viewer灰屏,黑屏,鼠标大黑叉等现象,详细见文档。

SpringBoot整合SpringSercurity5完整实现例子

super(msg);super(msg);return msg;@Component@OverrideString jsonString = JSONObject.toJSONString(ResponseResult.error(401, "您没有访问权限,请重新登录!"));

Spell a的博客 753

Statistics and Machine Learning Toolbox

Statistics and Machine Learning Toolbox(带书签),MATLAB统计学习工具

SpringBoot2整合activiti7

目录 1、创建SpringBoot2 工程 2、引入 maven 依赖 3、主配置文件 4、创建数据库 5、主程序 6、测试程序,生成25张表 7、创建流程定义图 8、创建SpringSecurity工具类 9、创建SpringSecurity配置类 10、查看流程定义 11、启动流程 12、查看并完成任务 1、创建SpringBoot2 工程 使用 idea,创建SpringBoot2 工程,坐标如下: <groupId>com.example&..

踏 浪的博客 1028

springboot+Activiti7整合实践 (一)

前端采用vue2,后端采用公司基础框架,springboot版本2.7.9,activit7版本 7.1.0.M4,主要借鉴ruoyi及芋道的工作流功能,完成工作流的定义、发布、流程启动、审批、转班、回退等功能。这里是springboot+activiti简单整合的步骤,其中遇到很多问题,主要集中在依赖冲突、springsecurity排除上,但总体没什么难度。

刘一线的博客 3464

activiti7和SpringBoot的整合

activiti和Springboot整合实例

qq_36071034的博客 1188

SpringSecurity系列——基于SpringBoot2.7的登录接口(内有惊喜)day2-1

先自我介绍一下,小编13年上师交大毕业,曾经在小公司待过,去过华为OPPO等大厂,18年进入阿里,直到现在。深知大多数初中级java工程师,想要升技能,往往是需要自己摸索成长或是报班学习,但对于培训机构动则近万元的学费,着实压力不小。因此我收集了一份《java开发全套学习资料》送给大家,初衷也很简单,就是希望帮助到想自学又不知道该从何学起的朋友,同时减轻大家的负担。这里解释一下,由于我直接使用了我自己写的JJWTUtil所以这里只要将该类是设置为配置类继承JJWTUtil即可。其实在官网里很明确的说了。..

李白 756

Spring Boot2.3整合activiti7 快速入门

Spring Boot2.3整合activiti7 activiti 工作流引擎,主要用于可灵活变动的系统的流程使用,使用activiti 管理自动化流程,摆脱用数据库状态为标识做流程,当流程改变还需要改代码,避免无用工作量 pom文件 <?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XM

zgc55987的博客 881

SpringBoot:整合SpringSecurity

为了提供安全的机制,Spring提供了其安全框架Spring Security,它是一个能够为基于Spring生态圈,提供安全访问控制解决方案的框架。它提供了一组可以在Spring应用上下文中配置的机制,充分利用了Spring的强大特性,为应用系统提供声明式的安全访问控制功能,减少了为企业系统安全控制编写大量重复代码的工作。

燕双嘤 2268
上一篇: memcached内容分配及工作原理
下一篇: 从实战出发,自定义一个springboot starter
ff-xiao
博客等级 码龄10年 99粉丝 36原创
评论 14
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值