SpringScurity+JWT实战讲解一(前后端分离)

一:配置(整合springsecurity+JWT的依赖)

1.pom.xml

 <!-- springboot security -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <!-- jwt -->
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
            <version>0.9.1</version>
        </dependency>
        <dependency>
            <groupId>com.github.axet</groupId>
            <artifactId>kaptcha</artifactId>
            <version>0.0.9</version>
        </dependency>
          <!-- hutool工具类-->
        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>5.3.3</version>
        </dependency>
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
            <version>3.11</version>
        </dependency>

启动redis,然后我们再启动项目,这时候我们再去访问http://localhost:8081/test,会发现系统会先判断到你未登录跳转到http://localhost:8081/login,因为security内置了登录页,用户名为user,密码在启动项目的时候打印在了控制台。登录完成之后我们才可以正常访问接口。 因为每次启动密码都会改变,所以我们通过配置文件来配置一下默认的用户名和密码:

spring:
  security:
    user:
      name: user
      password: 111111

二:定义redis的序列化规则JwtUtils

redis的工具类不需要自己写

package com.example.vueadmin.util;


import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

import java.util.Date;

@Data
@Component
@ConfigurationProperties(prefix = "vueadmin.jwt")
public class JwtUtils {
	private long expire;

	private String secret;
	private String header;

	// 生成jwt
	public String generateToken(String username){
		Date nowDate = new Date();

		Date expireDate = new Date(nowDate.getTime() + 1000*expire);

		return Jwts.builder()
				.setHeaderParam("typ","JWT")
				.setSubject(username)
				.setIssuedAt(nowDate)
				.setExpiration(expireDate)
				.signWith(SignatureAlgorithm.HS512,secret)
				.compact();

	}


	// 解析jwt

	public Claims getClaimByToken(String jwt){
		try{
			return Jwts.parser()
					.setSigningKey(secret)
					.parseClaimsJws(jwt)
					.getBody();
		}catch (Exception e){
			return null;
		}
	}
	// jwt是否过期

	public boolean isTokenExpired(Claims claims){
		return claims.getExpiration().before(new Date());
	}



}

定义一个redisconfig类,配置redis的序列化方式


import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;

@Configuration
public class RedisConfig {
    @Bean
    RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory){
        RedisTemplate redisTemplate = new RedisTemplate();
        redisTemplate.setConnectionFactory(redisConnectionFactory);

        Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
        jackson2JsonRedisSerializer.setObjectMapper(new ObjectMapper());

        redisTemplate.setKeySerializer(new StringRedisSerializer());
        redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);

        redisTemplate.setHashKeySerializer(new StringRedisSerializer());
        redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer);
        return redisTemplate;
    }
}

三:SpringScurity原理分析

详细讲解请看:添加链接描述 在这里插入图片描述 上面的图请好好看一下。针对我自己做的系统,设计出以下security的认证方案,大家直接拿来用就可以。 在这里插入图片描述

本系统的security的登录认证流程如下: 1.客户端发起一个请求,进入security过滤器链 2.当LogoutFilter的时候判断是否登陆路径,如果登出路径则到logoutHandler,如果登出成功则到logoutSucceshandler等处成功处理,如果不是登出路径则直接进入下一个过滤器。 3.当到UsernamePasswordAuthenticationFilter的时候判断是否为登录路径,如果是,则进入该过滤器进行登陆操作,如果登陆失败则到AuthenticationFailureHandler,登录失败处理器处理,如果登陆成功则到AuthenticationSuccesHandler登录成功处理器,如果不是登录请求则不进入该处理器。 4.进入认证BasicAuthenticationFilter进行用户认证,成功的话会把认证了的结果写入到SecurityContextHolder中SecuriyContext的属性authentiction上面。如果认证失败就交AuthenticationEntrypoint认证失败处理类,或者抛出异常 被后续ExceptionTranslationFilter过滤器处理异常,如果是AccessDeniedException异常交给AccessDeniedhandler处理。 5.当到FilterSecurityInterceptor的时候会拿到uri,根据uri去找对应的鉴权管理器、鉴权管理器做鉴权工作,鉴权成功则到Controller层,否则到AcccessDeniedhandler鉴权失败处理器处理。 具体的各个过滤器链:请看这个博客springSecurity所有的内置Filter,本人只做简单介绍系统中所用到的组件:

  • LogoutFilter——登出过滤器
  • logoutSuccessHandler——登出成功之后的操作类
  • UsernamePasswordAuthenticatioFilter——form提交用户名密码登录认证过滤器
  • AuthenticationFailurehandler——登录失败操作类
  • AuthenticationSuccessHandler——登陆成功操作类
  • BasicAuthenticationFilter_Basic身份认证过滤器
  • SecurityContextHolder——安全上下静态工具类
  • AuthenticationEntryPoint - 认证失败入口
  • ExceptionTranslationFilter - 异常处理过滤器
  • AccessDeniedHandler - 权限不足操作类
  • FilterSecurityInterceptor - 权限判断拦截器、出口

四:用户认证——用户验证码登录

在这里插入图片描述

1.配置图片验证码的生成规则:

import com.google.code.kaptcha.impl.DefaultKaptcha;
import com.google.code.kaptcha.util.Config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.core.parameters.P;

import java.util.Properties;

@Configuration
public class KaptchaConfig {

	@Bean
	DefaultKaptcha producer() {
		Properties properties = new Properties();
		properties.put("kaptcha.border", "no");
		properties.put("kaptcha.textproducer.font.color", "black");
		properties.put("kaptcha.textproducer.char.space", "4");
		properties.put("kaptcha.image.height", "40");
		properties.put("kaptcha.image.width", "120");
		properties.put("kaptcha.textproducer.font.size", "30");

		Config config = new Config(properties);
		DefaultKaptcha defaultKaptcha = new DefaultKaptcha();
		defaultKaptcha.setConfig(config);

		return defaultKaptcha;
	}

}

然后通过控制器提供生成验证码的方法:

@Slf4j
@RestController
public class AuthController extends BaseController{
   @Autowired
   private Producer producer;
   /**
    * 图片验证码
    */
   @GetMapping("/captcha")
   public Result captcha(HttpServletRequest request, HttpServletResponse response) throws IOException {
      String code = producer.createText();
      String key = UUID.randomUUID().toString();
      BufferedImage image = producer.createImage(code);
      ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
      ImageIO.write(image, "jpg", outputStream);
      BASE64Encoder encoder = new BASE64Encoder();
      String str = "data:image/jpeg;base64,";
      String base64Img = str + encoder.encode(outputStream.toByteArray());

      // 存储到redis中
      redisUtil.hset(Const.captcha_KEY, key, code, 120);
      log.info("验证码 -- {} - {}", key, code);
      return Result.succ(
            MapUtil.builder()
            .put("token", key)
            .put("base64Img", base64Img)
            .build()
      );
   }
}

因为前后端分离,我们禁用了session,所以我们把验证码放在了redis中,使用一个随机字符串作为key,并传送到前端,前端再把随机字符串和用户输入的验证码提交上来,这样我们就可以通过随机字符串获取到保存的验证码和用户的验证码进行比较了是否正确了。 然后因为图片验证码的方式,所以我们进行了encode,把图片进行了base64编码,这样前端就可以显示图片了。 2.图片验证码进行认证验证码是否正确。

  • CaptchaFilter
package com.example.vueadmin.security;

import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import com.example.vueadmin.common.exception.CaptchaException;
import com.example.vueadmin.common.lang.Const;
import com.example.vueadmin.util.RedisUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

@Component
public class CaptchaFilter extends OncePerRequestFilter {

    @Autowired
    RedisUtil redisUtil;

    @Autowired
    LoginFailureHandler loginFailureHandler;

    @Override
    protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {

        String url = httpServletRequest.getRequestURI();

        if("/login".equals(url)  && httpServletRequest.getMethod().equals("POST")){
            try{
                validate(httpServletRequest);
            }catch (CaptchaException e){
                // 交给认证失败的处理器
                loginFailureHandler.onAuthenticationFailure(httpServletRequest, httpServletResponse,e);
            }
            // 校验验证码

        }

        filterChain.doFilter(httpServletRequest,httpServletResponse);
    }

    // 校验验证码逻辑
    private void validate(HttpServletRequest httpServletRequest) {
        String code = httpServletRequest.getParameter("code");
        String key = httpServletRequest.getParameter("token");
        if(StringUtils.isBlank(code) || StringUtils.isBlank(key)){
            throw new CaptchaException("验证码错误");
        }


        if(!code.equals(redisUtil.hget(Const.CAPTCHA_KEY,key))){
            throw new CaptchaException("验证码错误");
        }
        // 一次性使用
        redisUtil.hdel(Const.CAPTCHA_KEY,key);

    }
}

验证码出错的时我们返回异常信息,这是一个认证异常,所以我们自定了一个CaptchaException:

  • com.exampler.vueadmin.common.exception.CaptchaException
public class CaptchaException extends AuthenticationException {
   public CaptchaException(String msg) {
      super(msg);
   }
}
  • com.exampler.vueadmin.common.lang.Const
public class Const {
   public static final String captcha_KEY = "captcha";
}

然后认证失败的话,我们之前说过,登录失败的时候交给AuthenticationFailureHandler,所以我们自定义了LoginFailureHandler

  • com.exampler.vueadmin.security.LoginFailureHandler
@Component
public class LoginFailureHandler implements AuthenticationFailureHandler {
   @Override
   public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
      response.setContentType("application/json;charset=UTF-8");
      ServletOutputStream outputStream = response.getOutputStream();
      Result result = Result.fail(
            "Bad credentials".equals(exception.getMessage()) ? "用户名或密码不正确" : exception.getMessage()
      );
      outputStream.write(JSONUtil.toJsonStr(result).getBytes("UTF-8"));
      outputStream.flush();
      outputStream.close();
   }
}

其实主要就是获取异常的消息,然后封装到Result,最后转成json返回给前端而已哈。 然后我们配置SecurityConfig

  • com.exampler.vueadmin.config.SecurityConfig
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
   @Autowired
   LoginFailureHandler loginFailureHandler;

   @Autowired
   CaptchaFilter captchaFilter;

   public static final String[] URL_WHITELIST = {
         "/webjars/**",
         "/favicon.ico",

"/captcha",
         "/login",
         "/logout",
   };

   @Override
   protected void configure(HttpSecurity http) throws Exception {
      http.cors().and().csrf().disable()
            .formLogin()
            .failureHandler(loginFailureHandler)

            .and()
            .authorizeRequests()
            .antMatchers(URL_WHITELIST).permitAll() //白名单
            .anyRequest().authenticated()
            // 不会创建 session
            .and()
            .sessionManagement()
            .sessionCreationPolicy(SessionCreationPolicy.STATELESS)

            .and()
            .addFilterBefore(captchaFilter, UsernamePasswordAuthenticationFilter.class) // 登录验证码校验过滤器
      ;
   }
}

首先formLogin我们定义了表单登录提交的方式以及定义了登录失败的处理器,后面我们还要定义登录成功的处理器的。然后authorizeRequests我们除了白名单的链接之外其他请求都会被拦截。再然后就是禁用session,最后是设定验证码过滤器在登录过滤器之前。 对于跨域问题的解决

@Configuration
public class CorsConfig implements WebMvcConfigurer {
   private CorsConfiguration buildConfig() {
      CorsConfiguration corsConfiguration = new CorsConfiguration();
      corsConfiguration.addAllowedOrigin("*");
      corsConfiguration.addAllowedHeader("*");
      corsConfiguration.addAllowedMethod("*");
      corsConfiguration.addExposedHeader("Authorization");
      return corsConfiguration;
   }

   @Bean
   public CorsFilter corsFilter() {
      UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
      source.registerCorsConfiguration("/**", buildConfig());
      return new CorsFilter(source);
   }

   @Override
   public void addCorsMappings(CorsRegistry registry) {
      registry.addMapping("/**")
            .allowedOrigins("*")
//          .allowCredentials(true)
            .allowedMethods("GET", "POST", "DELETE", "PUT")
            .maxAge(3600);
   }
}

猜你喜欢

转载自juejin.im/post/7124977540120182820