Spring Boot+Spring Security + JWT + Redis实现登录验证

springboot版本:2.4.3

创建项目啥的我就不说了,大家都看这个了,相信创建项目都轻而易举了,直接进入正题;

1:添加JWT和Spring Security依赖,多添加一个validation校验和lombok

		
		
			io.jsonwebtoken
			jjwt
			0.9.1
		

		
		
			org.springframework.boot
			spring-boot-starter-security
		

        
		
			org.springframework.boot
			spring-boot-starter-validation
		

		
		
			org.projectlombok
			lombok
			true
		

2:定义用户访问无权限资源时候的异常

@Component
public class JwtAccessDeniedHandler implements AccessDeniedHandler {
    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException e) throws IOException, ServletException {
        response.setCharacterEncoding("UTF-8");
        response.setContentType("application/json");
        response.setStatus(403);
        PrintWriter printWriter = response.getWriter();
        printWriter.write(new ObjectMapper().writeValueAsString(Result.fail("权限不足,请联系管理员!")));
        printWriter.flush();
        printWriter.close();
    }
}

3:定义用户权限不足的异常

@Component
public class JwtAccessDeniedHandler implements AccessDeniedHandler {
    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException e) throws IOException, ServletException {
        response.setCharacterEncoding("UTF-8");
        response.setContentType("application/json");
        response.setStatus(403);
        PrintWriter printWriter = response.getWriter();
        printWriter.write(new ObjectMapper().writeValueAsString(Result.fail("权限不足,请联系管理员!")));
        printWriter.flush();
        printWriter.close();
    }
}

4:Spring Security白名单类,放开权限控制不需要验证Token等信息

public class SpringSecurityConstant {
	
	/**
     * 放开权限校验的接口
     */
    public static final String[] NONE_SECURITY_URL_PATTERNS = {   		
    		//后端的
            "/login"           
    };

}

5:定义一个过滤器,登录时候先走这个类获取请求的token,具体业务逻辑因人而异

public class JwtAuthenticationFilter extends OncePerRequestFilter {

	@Autowired
	private TokenUtil jwtTokenUtil;

	@Autowired
	private RedisUtil redisUtil;

	@Autowired
	private UserDetailsService userDetailsService;

	@Value("${jwt.tokenHeader}")
	private String tokenHeader;

	@Override
	protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
			throws ServletException, IOException {
		String authHeader = request.getHeader(tokenHeader);
		if (!StrUtil.isBlank(authHeader)) {

			String password = null;
			try {
				logger.debug("解析token");
				password = jwtTokenUtil.getUserNameFromToken(authHeader);
			} catch (Exception e) {
				throw new RuntimeException("token解密失败");
			}
			// token存在,但是未登录
			if (null != password && null == SecurityContextHolder.getContext().getAuthentication()) {
				// 登录
				logger.debug("登录loadUserByUsername的password");
				UserDetails userDetails = userDetailsService.loadUserByUsername(password);
				// 验证redis中的toekn是否过期,重新设置用户对象
				if (redisUtil.hasKey(password)) {
					String redisToken = (String) redisUtil.getObject(password);
					// 判断携带的token和redis中的是否一致,一致则放行,不一致则以redis为准,退出请求
					if (authHeader.equals(redisToken)) {
						UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(
								userDetails, null, userDetails.getAuthorities());
						authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
						SecurityContextHolder.getContext().setAuthentication(authenticationToken);
					}
				} else {
					// 清除spring security用户认证信息
					Authentication auth = SecurityContextHolder.getContext().getAuthentication();
					if (null != auth) {
						new SecurityContextLogoutHandler().logout(request, response, auth);
					}
				}
//					if (jwtTokenUtil.validateToken(authHeader, userDetails)) {
//						UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
//						authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
//						SecurityContextHolder.getContext().setAuthentication(authenticationToken);
//					}
			}
		}
		chain.doFilter(request, response);
	}
}

6:定义SecurityConfig的配置类

@Component
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
	
	@Autowired
	private JwtAuthenticationEntryPoint authenticationEntryPoint;

	@Autowired
	private JwtAccessDeniedHandler accessDeniedHandler;

	/**
	 * 实现自定义登录逻辑
	 * 
	 * @param auth
	 * @throws Exception
	 */
	@Override
	protected void configure(AuthenticationManagerBuilder auth) throws Exception {
		//将登录的密码加密
		auth.userDetailsService(userDetailsService()).passwordEncoder(passwordEncoder());
	}

	/**
     * 配置白名单
     * @param web
     * @throws Exception
     */
    @Override
    public void configure(WebSecurity web) throws Exception {
        web.ignoring()
                .mvcMatchers(SpringSecurityConstant.NONE_SECURITY_URL_PATTERNS);
    }


	/* Security的核心配置 */
	@Override
	protected void configure(HttpSecurity http) throws Exception {
		// 使用jwt,关闭csrf
		http.csrf().disable();
		// 基于token认证,关闭session
		http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
		// 配置白名单 除白名单以外的都进行认证
		http.authorizeRequests().anyRequest().authenticated();
		// 禁用缓存
		http.headers().cacheControl();
		// 添加jwt登录授权的过滤器
        http.addFilterBefore(authenticationFilter(),
        UsernamePasswordAuthenticationFilter.class);
		// 添加未授权和未登录的返回结果
		http.exceptionHandling().accessDeniedHandler(accessDeniedHandler)
				.authenticationEntryPoint(authenticationEntryPoint);
	}

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

	@Bean
	public JwtAuthenticationFilter authenticationFilter() {
		return new JwtAuthenticationFilter();
	}

}

7:定义前端请求的实体类

@Data
@Api("系统用户登录参数")
public class SysLoginVo {
	
	@ApiModelProperty(value = "登录账号",dataType = "String" ,required = true)
	@NotBlank(message = "登录账号不能为空")
	private String loginName;
	
	@ApiModelProperty(value = "密码",dataType = "String" ,required = true)
	@NotBlank(message = "密码不能为空")
    private String password;
	
	@ApiModelProperty(value = "用户类型",dataType = "String" ,required = true)
	@NotBlank(message = "用户类型不能为空")
    private String userType;

}

8:定义Entity实体类,实现UserDetails

@Component
@Data
public class SysUserEntity implements UserDetails{
	
	private static final long serialVersionUID = 1L;

	@ApiModelProperty("用户ID")
	private Long userId;
	
	@ApiModelProperty("部门ID")
	private String deptId;
	
	@ApiModelProperty("登录账号")
	private String loginName;
	
	@ApiModelProperty("用户类型(00系统用户 01注册用户)")
	private String userType;
	
	@ApiModelProperty("用户邮箱")
	private String email;
	
	@ApiModelProperty("手机号码")
	private String phonenumber;
	
	@ApiModelProperty("用户性别(0男 1女 2未知)")
	private Character sex;
	
	@ApiModelProperty("头像路径")
	private String avatar;
	
	@ApiModelProperty("密码")
	private String password;
	
	@ApiModelProperty("帐号状态(0正常 1停用)")
	private String status;

	@Override
	public Collection getAuthorities() {
		return null;
	}

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

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

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

	@Override
	public boolean isEnabled() {
		if(StringUtils.isNotBlank(this.getStatus()) && "0".equals(this.getStatus())) {
			return false;
		}
		return true;
	}
	
	@Override
	public String getUsername() {
		return this.getLoginName();
	}
}

9:定义Controller类,添加登录方法

@RestController
@Api("用户信息登录控制器")
public class LoginConyroller {

	@Autowired
	private UserService userService;
	
	@Autowired
	private RedisUtil redisUtil;

	@PostMapping("/sysLogin")
	@ApiOperation(value = "系统用户登录", httpMethod = "POST")
	public Result sysLogin(@RequestBody @Valid SysLoginVo sysLoginVo) {
		return userService.sysLogin(sysLoginVo);
	}

	
	@GetMapping("/logOut")
	@ApiOperation(value = "退出登录", httpMethod = "GET")
	public Result logOut(HttpServletRequest request, HttpServletResponse 
        response,Principal principal ) {
		//清除spring security用户认证信息
		Authentication auth = SecurityContextHolder.getContext().getAuthentication();
		//清除redis中的token信息
		redisUtil.delKey(principal.getName());
		if(null != auth) {
			new SecurityContextLogoutHandler().logout(request, response, auth);
		}
		return new Result(MessageConstant.SUCCESS_CODE, MessageConstant.LOGOUT_SUCCESS);
	}			

}

10:定义userService接口

public interface UserService {	     
   /**
     * 登录
     * @param loginVo
     * @return
     */
	
	Result sysLogin(SysLoginVo sysLoginVo);
}

11:定义UserServiceImpl 实现userService接口,完成登录的业务逻辑处理

@Service
public class UserServiceImpl implements UserService{
	private Logger logger = LoggerFactory.getLogger(this.getClass());

    @Override
	public Result sysLogin(SysLoginVo sysLoginVo) {
		//系统用户登录以*开头
		String username = "*" + sysLoginVo.getLoginName();
		//走SpringSecurity的方法
		UserDetails userDetails = userDetailsService.loadUserByUsername(username);
		//利用hutool工具类转成数据库中的对象
		SysUserEntity sysUserEntity = BeanUtil.copyProperties(userDetails, SysUserEntity.class);
		//获取MD5加密值
		String md5HexPassword = DigestUtils.md5Hex(sysLoginVo.getLoginName() + sysLoginVo.getPassword() + sysUserEntity.getSalt());
		logger.debug("获取到md5加密值");
		//拿着获取到的用户名和密码验证一遍
		if(userMapper.checkSysUserLogin(sysLoginVo.getLoginName(),md5HexPassword) ==  0 ) {
			logger.debug("用户名:{}和密码:{}校验未通过",sysLoginVo.getLoginName(),md5HexPassword);
			return Result.fail("用户名或密码错误");
		};
        //这里会调用SysUserEntity实体类中的isEnabled方法,根据业务逻辑自行判断账户状态
		if(userDetails.isEnabled()) {
			return Result.fail("该帐号已被禁用,请联系管理员!");
		}
		return baseLogin(userDetails,null, sysLoginVo,"sysUser");
	}
	

   @Transactional(rollbackFor = {Exception.class})
	private Result baseLogin(UserDetails userDetails, LoginVo loginVo,SysLoginVo sysLoginVo, String userRole) {
		HashMap map = new HashMap<>(16);
        //生成token
    	String token = tokenUtil.generateToken(userDetails,userRole);
        //将token和登陆时间放入map
    	map.put("token", token);
    	map.put("loginTime", DateUtil.format(new Date(),"yyyy-MM-dd HH:mm:ss"));
    	try {
	    	if("mpUser".equals(userRole)) {
	    		map.put("customerName", loginVo.getCustomerName());
	        	map.put("identityCard", loginVo.getIdentityCard());
	        	//更新用户登录时间
	        	userMapper.updateUserInfo(map);
	        	//存到登录日志
	        	map.put("id",System.currentTimeMillis());
		    	userMapper.insertLoginLog(map);
		    	//存进redis
		    	String redisKey = "#" + loginVo.getIdentityCard();
	        	redisUtil.setObjectTime(redisKey,token,expireMinutes);
	    	}else {
	    		map.put("loginName", sysLoginVo.getLoginName());
//	    		userMapper.updateSysUserInfo(map);不能更改对面的表
	    		//查询当前系统用户的信息
	    		Map sysUserInfo = userMapper.querySysUserBylogName(map);
	    		if(!sysUserInfo.get("user_type").equals(sysLoginVo.getUserType())) {
	    			return Result.fail("当前用户不属于该角色");
	    		}
	    		//如果是商家返回机构信息
	    		if("04".equals(sysLoginVo.getUserType())) {
	    			//查询当前系统用户对应的机构信息
		    		Map resultMap =  userMapper.getSysUserInfoDetail(sysUserInfo);
		    		if(resultMap != null) {
		    			map.put("businessCircleId",resultMap.get("business_circle_id"));
		    			map.put("id",resultMap.get("id"));
			    		map.put("orgId",resultMap.get("org_id"));
			    		map.put("sceneId",resultMap.get("scene_id"));
			    		map.put("orgName",resultMap.get("org_name"));
						map.put("orgPic",resultMap.get("org_pic"));
		    		}
	    		}
				//如果是商家返回机构信息
				if("03".equals(sysLoginVo.getUserType())) {
					//查询当前系统用户对应的机构信息
					String deptName=  userMapper.getSysUserDeptInfoDetail(sysUserInfo);
					map.put("deptName",deptName);
				}
	    		map.put("userName",sysUserInfo.get("user_name"));
	    		map.put("userId",sysUserInfo.get("user_id"));
	    		//存进redis
		    	String redisKey = "*" + sysLoginVo.getLoginName();
	    		redisUtil.setObjectTime(redisKey, token, expireMinutes);
	    	}
	    	
    		logger.debug("用户{}登录成功,信息已保存至redis和数据库",userDetails.getUsername());
            UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
    		SecurityContextHolder.getContext().setAuthentication(authenticationToken);
    		return Result.success(MessageConstant.LOGIN_SUCCESS, map);
    	}catch(Exception e) {
    		if("mpUser".equals(userRole)) {
    			redisUtil.delKey(loginVo.getCustomerName());
    		}else {
    			redisUtil.delKey(sysLoginVo.getLoginName());
    		}
    		logger.debug("用户{}登录失败,信息已回滚,{}",userDetails.getUsername(),e.getMessage());
    		return Result.fail(MessageConstant.LOGIN_FAIL);
    	}
	}

12:定义一个UserDetailsServiceImpl的类实现userService接口,完成自定义登录

@Service
public class UserDetailsServiceImpl implements UserDetailsService{
	
	private Logger logger = LoggerFactory.getLogger(this.getClass());
		
	@Autowired
	private UserMapper userMapper;

	@Autowired
	private SysLogInFoMapper sysLogInfoMapper;
	

	@Transactional(rollbackFor = Exception.class)
	@Override
	public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
		// 判断是小程序用户还是系统用户
		// 小程序用户登录以#开头
		if (username.startsWith("#")) {
			String identityCard = username.substring(username.indexOf("#") + 1);
			logger.debug("小程序用户身份证号为:{}", identityCard);
			// 查询小程序用户,identityCard为传过来的身份证号
			CreditCustomerEntity mpUser = userMapper.findByuserId(identityCard);
			// 小程序用户为空则去查成员表
			if (mpUser == null) {
				// 将成员表信息新增到小程序用户表
				Map map = userMapper.findByfrmMeber(identityCard);
				if (map == null) {
					throw new RuntimeException("当前用户信息不存在!");
				}
				map.put("login_time", sdf.format(new Date()));
				userMapper.insertCreditCustomer(map);
				logger.debug("小程序用户表新增信息成功:{}", map);
				return userMapper.findByuserId(identityCard);
			}
			return mpUser;
		} else if(username.startsWith("*")){
			String loginName = username.substring(username.indexOf("*") + 1);
			logger.debug("系统用户名为:{}", loginName);
			// 查询系统用户
			SysUserEntity sysUser = sysLogInfoMapper.querysUserByUserName(loginName);
			logger.info("系统用户信息:{}", sysUser);
			if (null == sysUser) {
				throw new RuntimeException("当前系统用户不存在!");
			}
			return sysUser;
		}else {
			throw new RuntimeException("暂未匹配到该用户信息");
		}
	}
}

你可能感兴趣的:(后端,spring,boot,spring,redis)