一、用户认证授权

在系统中最基础的就是用户登陆,一般系统用到的登陆有2种,分别是账号密码和手机验证码。本笔记以账号密码登陆为例,手机验证码登陆等整个框架基本搭好后再写。

1.1 需求分析

用户使用一个系统其实就是访问系统里的每一个功能,在浏览器中对应的就是一个个网页。一个正常的系统,那么肯定存在有些网页不需要权限,所有人都能访问,例如首页,登录页面,找回密码页面等;有些页面则需要用户登陆后才能访问,例如管理页面等。那么这就需要给用户授权,当然根据项目的实际需求,授权也分为功能授权和数据授权,这2块就比较复杂了,以后有机会再说,我这里只做登陆授权的案例。

1.2 单点登录

单点登陆其实就是只要登陆一次可以无缝访问多个相关子项目。具体的含义自行百度。

1.3 第三方认证

第三方认证技术方案最主要是解决认证协议的通用标准 问题,因为要实现 跨系统认证,各系统之间要遵循一定的接口协议。目前应用比较广泛的是Qauth2认证。

二、搭建认证服务

2.1基本需求

本案例认证服务基于Spring Security Oauth2进行构建,并在其基础上作了一些扩展,采用JWT令牌机制,并自定义了用户身份信息的内容。 关于Spring Security 、Oauth2、JWT 可以网上搜索资料,这里就不展开了。
我这里简单说下认证服务的逻辑,
1、用户填写用户名、密码提交到后台,后端按照Spring Security安全框架标准去判断用户名密码是否正确。
2、用户名密码填写正确则根据项目的实际情况做相应的业务处理,例如简单一点的就是获取用户的基本信息,复杂的就需要获取用户的角色权限和数据权限等。这些数据保存到Redis中。
3、在配置文件中设置登陆时效,并且通过定义一种标准来更新登陆时效,类似于最早的用session作为登陆权限判断时自带的过期时间。
4、同一个账号,同时只能登陆一个终端,后登录的会踢掉之前登陆者。

2.2 Redis 整合

2.2.1先在框架pom.xml中引入Redis

<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-data-redis</artifactId>
			<exclusions>
                <!-- 默认指定了 lettuce,需要排除并引入jedis的客户端-->
                <exclusion>
                    <groupId>io.lettuce</groupId>
                    <artifactId>lettuce-core</artifactId>
                </exclusion>
            </exclusions>
		</dependency>		
		<dependency>
			<groupId>redis.clients</groupId>
			<artifactId>jedis</artifactId>
		</dependency>

2.2.2 配置文件

application.yml
注意:为了简便,下面的配置中我将druid删除了,有需要的查看《笔记三》

spring:
  datasource:
    name: test
    url: jdbc:mysql://localhost:3306/test?useUnicode=true&serverTimezone=GMT&characterEncoding=utf-8&allowMultiQueries=true&useSSL=false
    username: root
    password: 123456
    # 使用druid数据源
    type: com.alibaba.druid.pool.DruidDataSource
    driver-class-name: com.mysql.cj.jdbc.Driver   
  redis:
    database: 1
    host: 127.0.0.1 
    port: 6379 
    password: 123456
    timeout: 5000
    jedis: 
      pool:
        max-active: 500
        max-wait: 5000
        max-idle: 400
        min-idle: 50

2.2.3 配置类

@Configuration
public class RedisConfig {
	@Bean
	public RedisTemplate<String, Object> redisTemplate(final RedisConnectionFactory factory) {
		final Jackson2JsonRedisSerializer<Object> j = new Jackson2JsonRedisSerializer<Object>(Object.class);		
		// value序列化
		final RedisTemplate<String, Object> redisTemplate = new RedisTemplate<String, Object>();
		redisTemplate.setValueSerializer(j);
		redisTemplate.setHashValueSerializer(j);
		// key序列化;(不然会出现乱码)
		RedisSerializer<?> stringSerializer = new StringRedisSerializer();
		redisTemplate.setKeySerializer(stringSerializer);
		redisTemplate.setHashKeySerializer(stringSerializer);
		redisTemplate.setConnectionFactory(factory);
		redisTemplate.afterPropertiesSet();
		return redisTemplate;
	}
}

2.2.4 Redis 工具类

可以参考前面的文章,限于篇幅,就不多说了。
《Springboot整合Redis(三) : 整合Redis》
《springboot集成redis高并发下同时保存获取字符串和对象时出现异常的解决办法》

2.2.5 测评Redis

@SpringBootTest(classes = { DemoApplication.class })
class DemoApplicationTests {
	@Autowired
	HnRedisUtils hnRedisUtils;
	@Test
	void add() {
		hnRedisUtils.set("username", "半路凉亭");
	}
}

运行add后,查看redis,如下图说明Redis集成成功

SpringBoot未授权RCE springboot 认证授权_SpringBoot未授权RCE

2.3 Spring Security

2.3.1 引入

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

本案例是通过账号密码方式做登陆认证的,而spring security框架本身就自带密码认证功能,只要按照框架的认证标准创建相应的步骤即可完成认证操作,非常方便。当然,在现实项目中可能除了需要用密码登陆外,还需要用手机验证码登陆,这块要按照spring security框架的结构自定义一些模块即可,后期有机会再写。

2.3.2 项目配置

一个项目的标准化登陆有以下几项设置:不用认证的页面、登陆的失效时间、是否可以多终端登陆等。
application.yml

token:
  expire:
    seconds: 7200 #登录失效时长 单位:秒
  jwtSecret: H@+N#-M*$S*7TA*R
login:
  isMulti: true #是否可以多点登录
  passwordParameter: password #修改登录的密码框名称,默认是password
  loginHTML: /login.html #登陆页面地址
  loginProcessingUrl: /login #登录处理地址,只是别名
  permitAllUrl: /, /*.html, /login/**, /druid/** #不做登录拦截的地址

2.3.3 基础配置

目前登陆认证使用比较广泛的是口令方式,运用比较多的就是oauth2+JWT,这块具体的原理可以看下面的文章,
《JSON Web Token 入门教程》

我们新建一个口令的类,目前只有一个属性,就是token

public class Token implements Serializable {
	private static final long serialVersionUID = -9149471519045934216L;
	private String token;
	public Token() {
		super();
	}
	public Token(final String token) {
		super();
		this.token = token;		
	}
	public String getToken() {
		return this.token;
	}
	public void setToken(final String token) {
		this.token = token;
	}
}

创建登陆的用户对象,并且实现spring security 的接口 UserDetails

public class CurrentLoginUser implements UserDetails {

	private static final long serialVersionUID = -6135362231034146981L;
	private String id;
	private String username;
	private String realname;
	private String password;
	private String redisKey;
	/** 登陆时间戳(毫秒) */
	private Long loginTime;
	/** 过期时间戳 */
	private Long expireTime;
	private Boolean enabled;
	private Boolean credentialsNonExpired;
	private Boolean accountNonLocked;
	private Boolean accountNonExpired;
	/**登录状态 normalLogin :正常 ; kickOut : 被踢 */
	private String loginType;
	private String isAdmin;
	//private String orgId;
	private Map<String, Object> paramsMap;
	}

说明:
username\password 是登陆的用户名和密码,spring security 会使用,此用户对象会写入redis中,安全考虑,再写入前将密码设为NULL。除了这2个字段外,其余字段理论上都不是必须的。
realname :登陆用户的真实姓名。
redisKey: 登陆成功后,当前用户在redis中对应的key,这个也是生成JWT的关键。
loginTime、expireTime :这2个时间就是有效期,根据具体的算法和配置有相应的改变,例如我一般是设置成登陆后有2个小时有效期,时间过了一半且未超过2小时,用户还在操作,那么有效期自动延长2小时,登陆的总有效时长可以在配置文件中设置,即上面的seconds: 7200。
isAdmin: 是否是超级管理员,在实际项目中,可用于超管权限,无视权限配置,特殊的一个字段,跟具体的业务关联。
paramsMap:额外的参数,1.具体的业务可能有额外的登陆需求 2.手机验证登陆使用。

2.3.4 token服务层

登陆操作成功后常规分为创建口令即token ,刷新token, 删除token,通过token查询登陆用户信息等,所以这里创建一个token的操作层,直接给代码。

定义一个接口

public interface IHnTokenService {
	/**
	 * 生成token
	 *
	 * @author huangmin
	 * @param loginUser
	 * @return
	 */
	Token saveToken(CurrentLoginUser loginUser);
	/**
	 * 刷新token
	 *
	 * @author huangmin
	 * @param loginUser
	 */
	void refresh(CurrentLoginUser loginUser);
	/**
	 * 通过token获取当前登陆用户
	 *
	 * @author huangmin	
	 * @param token
	 * @return
	 */
	CurrentLoginUser getCurrentUser(String token);
	CurrentLoginUser getCurrentUser(HttpServletRequest request);
	/**
	 * 删除TOKEN并将redis的缓存删除
	 *
	 * @author huangmin
	 * @param token
	 * @return
	 */
	boolean deleteToken(String token);
}

实现类:

@Primary
@Service
public class HnTokenJWTServiceImpl implements IHnTokenService {

	/**
	 * token过期秒数
	 */
	@Value("${token.expire.seconds}")
	private Integer expireSeconds;
	/**
	 * 私钥
	 */
	@Value("${token.jwtSecret}")
	private String jwtSecret;
	/**
	 * 是否可以多点登录
	 */
	@Value("${login.isMulti}")
	private Boolean isMulti;
	@Autowired
	private HnRedisUtils redisUtils;
	private static Key KEY = null;
	private static final String LOGIN_USER_KEY = "LOGIN_USER_KEY";

	@Override
	public Token saveToken(final CurrentLoginUser loginUser) {
		loginUser.setRedisKey(UUID.randomUUID().toString());
		this.cacheLoginUser(loginUser);
		final String jwtToken = this.createJWTToken(loginUser);		
		return new Token(jwtToken);
	}

	/**
	 * 生成jwt
	 *
	 * @param loginUser
	 * @return
	 */
	private String createJWTToken(final CurrentLoginUser loginUser) {
		final Map<String, Object> claims = new HashMap<>();
		claims.put(HnTokenJWTServiceImpl.LOGIN_USER_KEY, loginUser.getRedisKey());// 放入一个随机字符串,通过该串可找到登陆用户
		final String jwtToken = Jwts.builder().setClaims(claims)
				.signWith(SignatureAlgorithm.HS256, this.getKeyInstance()).compact();		
		return jwtToken;
	}

	/**
	 * 将loginUser 缓存到Redis
	 *
	 * @author huangmin
	 * @param loginUser
	 */
	private void cacheLoginUser(final CurrentLoginUser loginUser) {
		final long currentTimeMillis = System.currentTimeMillis();
		final Long loginUserTime = loginUser.getLoginTime();
		if (loginUserTime == null) {
			loginUser.setLoginTime(currentTimeMillis);
		}
		loginUser.setExpireTime(currentTimeMillis + this.expireSeconds * 1000);
		// 根据uuid将loginUser缓存
		this.redisUtils.set(this.getTokenKey(loginUser.getRedisKey()), loginUser);
		// loginId:id
		final String key = this.getLoginIdKey(loginUser.getId());
		Set<String> set = new HashSet<String>();
		// 获取的key存在
		if (this.redisUtils.containsKey(key)) {
			set = this.redisUtils.getObject(key, Set.class);
		}
		// 不能多点登录,将所有以前登录存入的KEY全部踢掉
		// 将key组成一个SET放到以用户ID为KEY的变量里,并存入Redis
		// 将所有以前登陆时存入的KEY全部踢掉
		if (!this.isMulti) {
			for (final Object object : set) {
				final String redisKey = String.valueOf(object);
				if (HnStringUtils.equals(redisKey, loginUser.getRedisKey())) {
					continue;
				}
				// 将其余登录的账号全部踢出
				final String reKey = this.getTokenKey(redisKey);
				final CurrentLoginUser user = this.redisUtils.getObject(reKey, CurrentLoginUser.class);
				if (user == null) {
					continue;
				}
				user.setLoginType("kickOut");
				this.redisUtils.set(reKey, user);
			}
			set = new HashSet<String>();
		}
		set.add(loginUser.getRedisKey());
		this.redisUtils.set(key, set);
	}

	/**
	 * 更新缓存的用户信息
	 */
	@Override
	public void refresh(final CurrentLoginUser loginUser) {
		this.cacheLoginUser(loginUser);
	}

	@Override
	public CurrentLoginUser getCurrentUser(final String jwtToken) {
		try {
			final String key = this.getReditKeyFromJWTToken(jwtToken);
			if (HnStringUtils.isNotBlank(key)) {
				return this.redisUtils.getObject(this.getTokenKey(key), CurrentLoginUser.class);
			}
		} catch (final Exception e) {
			e.printStackTrace();
			throw new HnException(e);
		}
		return null;
	}

	@Override
	public CurrentLoginUser getCurrentUser(final HttpServletRequest request) {
		final String jwtToken = TokenFilter.getToken(request);
		return this.getCurrentUser(jwtToken);
	}

	@Override
	public boolean deleteToken(final String jwtToken) {
		final String redisKey = this.getReditKeyFromJWTToken(jwtToken);
		if (HnStringUtils.isNotBlank(redisKey)) {
			final String key = this.getTokenKey(redisKey);
			final CurrentLoginUser loginUser = this.redisUtils.getObject(key, CurrentLoginUser.class);
			this.redisUtils.delete(key);
			if (loginUser != null) {
				return true;
			}
		}
		return false;
	}

	/**
	 * 通过jwt获取redis中的token的ID
	 *
	 * @author huangmin
	 * @date 2021年8月6日
	 * @param jwtToken
	 * @return
	 */
	public String getReditKeyFromJWTToken(final String jwtToken) {
		if (HnStringUtils.isBlankOrNULL(jwtToken)) {
			return null;
		}
		try {
			final Map<String, Object> jwtClaims = Jwts.parser().setSigningKey(this.getKeyInstance())
					.parseClaimsJws(jwtToken).getBody();
			return String.valueOf(jwtClaims.get(HnTokenJWTServiceImpl.LOGIN_USER_KEY));
		} catch (final Exception e) {
			throw new HnException("口令已过期");
		}
	}

	private Key getKeyInstance() {
		if (HnTokenJWTServiceImpl.KEY == null) {
			synchronized (HnTokenJWTServiceImpl.class) {
				if (HnTokenJWTServiceImpl.KEY == null) {// 双重锁
					final byte[] apiKeySecretBytes = DatatypeConverter.parseBase64Binary(this.jwtSecret);
					HnTokenJWTServiceImpl.KEY = new SecretKeySpec(apiKeySecretBytes,
							SignatureAlgorithm.HS256.getJcaName());
				}
			}
		}		
		return HnTokenJWTServiceImpl.KEY;
	}

	private String getLoginIdKey(final String loginId) {
		return "loginId:" + loginId;
	}

	private String getTokenKey(final String token) {
		return "tokens:" + token;
	}
}

说明:每一个用户登陆成功后会在redis中写入至少2条信息,其中一条是以自身ID为KEY的数据,保存的值就是对应的用于生成JWT的随机码,这里用的是UUID生成规则;另一条数据就是UUID作为KEY,对应的值就是登陆者对象信息,也即上面定义的CurrentLoginUser。

成功后的redis中的场景如下图所示

SpringBoot未授权RCE springboot 认证授权_oauth2_02


SpringBoot未授权RCE springboot 认证授权_SpringBoot未授权RCE_03

2.3.5 spring security 处理器

设置登陆操作的成功,失败,异常,以及退出成功的处理器,spring security 框架本身定义了一系列的标准Handler.

@Configuration
public class SecurityHandlerConfig {

	@Autowired
	private IHnTokenService tokenService;

	/**
	 * 登陆成功,返回Token
	 *
	 * @return
	 */
	@Bean
	public AuthenticationSuccessHandler loginSuccessHandler() {
		return new AuthenticationSuccessHandler() {

			@Override
			public void onAuthenticationSuccess(
					final HttpServletRequest request,
					final HttpServletResponse response,
					final Authentication authentication) throws IOException, ServletException {
				final CurrentLoginUser loginUser = (CurrentLoginUser) authentication.getPrincipal();
				final SucceedResponse succeedResponse = new SucceedResponse();
				loginUser.setPassword("");
				loginUser.setLoginType("normalLogin");				
				final Token token = SecurityHandlerConfig.this.tokenService.saveToken(loginUser);
				succeedResponse.addAttribute("token", token.getToken());
				HnResponseUtils.responseJson(response, HttpStatus.OK.value(), succeedResponse);
			}
		};
	}

	/**
	 * 登陆失败
	 *
	 * @return
	 */
	@Bean
	public AuthenticationFailureHandler loginFailureHandler() {
		return new AuthenticationFailureHandler() {

			@Override
			public void onAuthenticationFailure(
					final HttpServletRequest request,
					final HttpServletResponse response,
					final AuthenticationException exception) throws IOException, ServletException {
				String code = null;
				if (exception instanceof BadCredentialsException) {
					code = "passwordError";// 密码错误
				} else {
					code = exception.getMessage();
				}
				HnResponseUtils.responseJson(response, HttpStatus.OK.value(), new ErrorResponse(code));
			}
		};
	}

	/**
	 * 未登录,返回200,主要是为了前端好统一处理
	 *
	 * @return
	 */
	@Bean
	public AuthenticationEntryPoint authenticationEntryPoint() {
		return new AuthenticationEntryPoint() {

			@Override
			public void commence(
					final HttpServletRequest request,
					final HttpServletResponse response,
					final AuthenticationException authException) throws IOException, ServletException {
				final String code = HnStringUtils.equals(authException.getMessage(), "kickOut") ? "kickOut" : "unLogin";
				HnResponseUtils.responseJson(response, HttpStatus.OK.value(), new ErrorResponse(code));
			}
		};
	}
	/**
	 * 退出处理
	 *
	 * @return
	 */
	@Bean
	public LogoutSuccessHandler logoutSussHandler() {
		return new LogoutSuccessHandler() {
			@Override
			public void onLogoutSuccess(
					final HttpServletRequest request,
					final HttpServletResponse response,
					final Authentication authentication) throws IOException, ServletException {
				final String token = TokenFilter.getToken(request);
				SecurityHandlerConfig.this.tokenService.deleteToken(token);
				HnResponseUtils.responseJson(response, HttpStatus.OK.value(), new SucceedResponse("logout"));
			}
		};
	}
}

以上所有的处理器返回给前端的内容都是code =200,前端判断成功与否的标准以自定义对象AjaxResponse 的"succeed"为 true还是false。这样处理,前端在解析后端返回的数据时好统一处理。

2.3.6 token 过滤器

此类类似于以前的登陆拦截器,也即把每个操作都做登陆验证

@Component
public class TokenFilter extends OncePerRequestFilter {

	@Autowired
	private IHnTokenService tokenService;
	/**
	 * token过期秒数
	 */
	@Value("${token.expire.seconds}")
	private Integer expireSeconds;
	public static final String TOKEN_KEY = "token";
	@Autowired
	private AuthenticationEntryPoint authenticationEntryPoint;

	@Override
	protected void doFilterInternal(
			final HttpServletRequest request,
			final HttpServletResponse response,
			final FilterChain filterChain) throws IOException, ServletException {
		final String token = TokenFilter.getToken(request);
		// System.out.println("==============进入token过滤器" + token);
		if (HnStringUtils.isNotBlank(token)) {
			CurrentLoginUser loginUser = this.tokenService.getCurrentUser(token);
			if (loginUser != null) {
				try {
					loginUser = this.checkLoginUser(loginUser, token);
				} catch (final AuthenticationException e) {
					SecurityContextHolder.clearContext();
					this.authenticationEntryPoint.commence(request, response, e);
					return;
				}				
				final UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(
						loginUser, null, loginUser.getAuthorities());
				SecurityContextHolder.getContext().setAuthentication(authentication);
			}
		}
		filterChain.doFilter(request, response);
	}

	/**
	 * 校验登录的账号,如过期时间、是否被踢出<br>
	 * 过期时间与当前时间对比<br>
	 * 如果已经过期则删除token
	 *
	 * @param loginUser
	 * @return
	 * @throws Exception
	 */
	private CurrentLoginUser checkLoginUser(final CurrentLoginUser loginUser, final String token) {
		final long expireTime = loginUser.getExpireTime();
		final long currentTime = System.currentTimeMillis();
		final long time = expireTime - currentTime;
		final String loginType = loginUser.getLoginType();
		// 不是正常登陆则抛异常 AuthenticationException,此异常在SecurityHandlerConfig 异常处理Handler 的 authenticationEntryPoint中获取到
		if (!HnStringUtils.equals(loginType, "normalLogin")) {
			this.tokenService.deleteToken(token);
			throw new HnAuthenticationException(loginType);
		}
		if (time <= 0) {
			// System.out.println("****登陆失效");
			// 已经过期则删除token和redis对应的用户
			this.tokenService.deleteToken(token);
			throw new HnException("unLogin");
		} else if (this.expireSeconds * 1000 / 2 >= time) {			
			this.tokenService.refresh(loginUser);			
		}
		return loginUser;
	}

	/**
	 * 根据参数或者header获取token
	 *
	 * @param request
	 * @return
	 */
	public static String getToken(final HttpServletRequest request) {
		String token = request.getParameter(TokenFilter.TOKEN_KEY);
		if (HnStringUtils.isBlank(token)) {
			token = request.getHeader(TokenFilter.TOKEN_KEY);
		}
		return token;
	}

}

2.3.7 spring security 配置类

这个类是关键,主要是加载配置文件的参数,密码的加密方式,以及如何将配置注入到spring security 安全框架中。

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

	@Autowired
	UserDetailsService userDetailsService;
	@Autowired
	private AuthenticationSuccessHandler loginSuccessHandler;
	@Autowired
	private AuthenticationFailureHandler loginFailureHandler;
	@Autowired
	private AuthenticationEntryPoint authenticationEntryPoint;
	@Autowired
	private LogoutSuccessHandler logoutSuccessHandler;
	@Autowired
	private TokenFilter tokenFilter;
	/**
	 * 登陆页面
	 */
	@Value("${login.loginHTML}")
	private String loginHtml;
	/**
	 * 登陆后台处理页面
	 */
	@Value("${login.loginProcessingUrl}")
	private String loginProcessingUrl;
	/**
	 * 不拦截的页面
	 */
	@Value("${login.permitAllUrl}")
	private String permitAllUrl;
	/**
	 * 密码字段名称,默认password
	 */
	@Value("${login.passwordParameter}")
	private String passwordParameter;

	public String[] getPermitAllUrl() {
		final String str = this.permitAllUrl.replaceAll(" ", "");
		return str.split(",");
	}

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

	@Override
	protected void configure(final HttpSecurity http) throws Exception {
		http.csrf().disable();
		if (HnStringUtils.isBlank(this.passwordParameter)) {
			this.passwordParameter = "password";
		}

		// 基于token,所以不需要session
				http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);		http.authorizeRequests().antMatchers(this.getPermitAllUrl()).permitAll().anyRequest().authenticated();		http.formLogin().loginPage(this.loginHtml).loginProcessingUrl(this.loginProcessingUrl)				.passwordParameter(this.passwordParameter).successHandler(this.loginSuccessHandler)
				.failureHandler(this.loginFailureHandler).and().exceptionHandling()
				.authenticationEntryPoint(this.authenticationEntryPoint).and().logout().logoutUrl("/logout")
				.logoutSuccessHandler(this.logoutSuccessHandler);
		// 解决不允许显示在iframe的问题
		// http.headers().frameOptions().disable();
		// http.headers().cacheControl();

		http.addFilterBefore(this.tokenFilter, UsernamePasswordAuthenticationFilter.class);
	}

	@Override
	protected void configure(final AuthenticationManagerBuilder auth) throws Exception {		auth.userDetailsService(this.userDetailsService).passwordEncoder(this.passwordEncoder());
	}
}

2.3.8 登陆操作的实现

以上就是使用Spring Security 、Oauth2、JWT实现登陆操作的主要代码,这些代码只要写在自己自定义的框架中,以后的项目只要是用帐号、密码登录的都可以统一使用,不用每次都自己写了,不同的项目只要编写符合自己登录逻辑的业务层即可,我这里只做个最简单的登陆判断的案例。
在service层编写一个实现接口UserDetailsService的类

@Service
public class UserDetailsServiceImpl implements UserDetailsService {
	@Autowired
	private ISysUserService sysUserService;
	@Override
	public UserDetails loadUserByUsername(final String username) throws UsernameNotFoundException {		
		System.out.println("==========" + new BCryptPasswordEncoder().encode("123456"));
		SysUser user = new SysUser();
		user.setUsername(username);
		user = this.sysUserService.getOne(user);
		if (user == null) {
			// "用户名不存在"
			final String code = "usernameNonExist";
			throw new HnException(code);
		}
		final CurrentLoginUser loginUser = this.checkUser(user);
		final Map<String, Object> map = new HashMap<String, Object>();
		map.put("sex", user.getSex());
		loginUser.setParamsMap(map);
		return loginUser;
	}
	/**
	 * 验证当前用户及获取角色、组织等信息 <br>
	 * 根据项目实际情况获取
	 *
	 * @author huangmin
	 * @date 2021年6月7日
	 * @param user
	 * @return
	 */
	private CurrentLoginUser checkUser(final SysUser user) {
		// 后台的路由资源权限		
		// 获取用户的角色
		// 获取用户的组织
		final CurrentLoginUser loginUser = new CurrentLoginUser();
		loginUser.setId(user.getId());
		loginUser.setUsername(user.getUsername());
		loginUser.setPassword(user.getPassword());// 要将密码写入
		loginUser.setLoginTime(System.currentTimeMillis());
		return loginUser;
	}
}

上面的代码就是一个最简单的登陆操作,需要注意的是,只要在checkUser(final SysUser user) 的这个方法中将密码存放到要返回的用户对象里,这样spring security框架会自动进行密码对比。
如果在实际的项目中需要做其它的逻辑处理,都可以在这里编写,例如权限,角色的获取等。

测试的效果如下

账号,密码错误

SpringBoot未授权RCE springboot 认证授权_JWT_04


登陆成功

SpringBoot未授权RCE springboot 认证授权_用户认证授权_05


被踢出情况

SpringBoot未授权RCE springboot 认证授权_JWT_06


注意:此处要将配置文件中的isMulti设为false;

登陆2次后用第一次的token进行操作,就会提示上图中的code:kickOut

大功告成!!!