SpringBoot2+SpringSecurity+JWT完成安全认证

1. 写在前面

如何你能看到这边文章,那我觉得我也不需要多废话,文章里面的东西你自然可以看懂。此篇文章只介绍整合过程,不介绍原理,适合懂原理想快速搭建环境的人儿们。
  先晒一下项目目录:在这里插入图片描述

2. pom.xml

     <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.0.1.RELEASE</version>
    </parent>
    <dependencies>
        <!--        jwt相关依赖-->
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt-api</artifactId>
            <version>0.10.8</version>
        </dependency>
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt-impl</artifactId>
            <version>0.10.8</version>
        </dependency>
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt-jackson</artifactId>
            <version>0.10.8</version>
        </dependency>

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

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

        <!--        lombok-->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.8</version>
        </dependency>
    </dependencies>

3. application.yml

server:
  port: 7100

jwt:
  #http请求头
  header: Authorization
  #token起始标识
  start-with: Bearer
  #秘钥
  secret-key: MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAKiuOGSVzac1HH5x3cboD0KapCayatw4G6W59E9Vez6fwix2g5hUFmzRknoZiDREuAXkVi1MqAiQ7Wf8MqqPDvsCAwEAAQ==
  #过期时间 单位/秒
  validate-second: 43200

4. 创建配置类,映射yml中的变量

/**
 * @author : LCheng
 * @date : 2020-11-26 16:52
 * description : jwt配置参数
 */
@Data
@ConfigurationProperties(prefix = "jwt")
@Configuration
public class JwtProperties {

    /**
     * http请求头
     */
    private String header;

    /**
     * token起始标识
     */
    private String startWith;

    /**
     * token秘钥
     */
    private String secretKey;

    /**
     * token过期时间 单位/秒
     */
    private Long validateSecond;
}

5. 创建AccessDeniedHandler类

SecurityAccessDeniedHandler 用来处理无访问权限的请求,这里只是返回403的操作,可以根据系统来开发相应的功能

/**
 * @author : LCheng
 * @date : 2020-11-26 16:56
 * description : 无访问权限处理类
 */
@Component
public class SecurityAccessDeniedHandler implements AccessDeniedHandler {
    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException {
        //根据系统要求开发功能代码
        response.sendError(HttpServletResponse.SC_FORBIDDEN, "您的权限不足。");
    }
}

6.创建AuthenticationEntryPoint类

SecurityAuthenticationEntryPoint 用来处理认证失败的请求,这里只是返回401的操作,可以根据系统来开发相应的功能

/**
 * @author : LCheng
 * @date : 2020-11-26 16:58
 * description : 认证失败处理类
 */
@Component
public class SecurityAuthenticationEntryPoint implements AuthenticationEntryPoint {
    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException {
        //根据系统要求开发功能代码
        response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "认证失败。");
    }
}

7.创建jwt工具类


/**
 * @author : LCheng
 * @date : 2020-11-26 16:59
 * description : jwt工具
 */
@Component
@Slf4j
public class JwtUtil {

    @Autowired
    private JwtProperties jwtProperties;

    public static final String USER_KEY = "user";

    /**
     * 从request中获取token
     *
     * @param request
     * @return {@link String}
     * @author LCheng
     * @date 2020/11/26 17:15
     */
    public String getToken(HttpServletRequest request) {
        String token = "";
        String bearerToken = request.getHeader(jwtProperties.getHeader());
        if (StringUtils.hasText(bearerToken) && bearerToken.startsWith(jwtProperties.getStartWith())) {
            token = bearerToken.substring(jwtProperties.getStartWith().length());
        }
        return token;
    }

    /**
     * 根据token获取AuthenticationToken
     *
     * @param token
     * @return {@link UsernamePasswordAuthenticationToken}
     * @author LCheng
     * @date 2020/11/26 18:16
     */
    public UsernamePasswordAuthenticationToken getAuthentication(String token) {
        Claims claims = validate(token);
        if (claims == null) {
            return null;
        }
        
        HashMap map = (HashMap) claims.get(USER_KEY);
        Collection<? extends GrantedAuthority> authorities =
                ((List<Map<String, String>>) map.get("authorities")).stream()
                        .map(a -> new SimpleGrantedAuthority(a.get("authority")))
                        .collect(Collectors.toList());
        User principal = new User((String) map.get("username"), "", authorities);
        return new UsernamePasswordAuthenticationToken(principal, "", authorities);
    }

    /**
     * 生成jwt
     *
     * @param user
     * @author LCheng
     * @date 2020/11/26 17:15
     */
    public String generate(User user) {
        String token = Jwts.builder()
                .claim(USER_KEY, user)
                .setIssuedAt(new Date())
                .setExpiration(new Date(System.currentTimeMillis() + jwtProperties.getValidateSecond() * 1000))
                .signWith(SignatureAlgorithm.HS256, jwtProperties.getSecretKey()).compact();
        return token;
    }


    /**
     * 校验token
     *
     * @param token
     * @return {@link java.lang.Boolean}
     * @author LCheng
     * @date 2020/11/26 17:16
     */
    public Claims validate(String token) {
        try {
            return Jwts.parser().setSigningKey(jwtProperties.getSecretKey()).parseClaimsJws(token).getBody();
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }

}

8.创建SecurityAuthenticationTokenFilter

此过滤器用来拦截请求,并解析token,并将解析的用户信息保存到spring security作用域中

/**
 * @author : LCheng
 * @date : 2020-11-21 14:36
 * description : token验证的过滤器
 */
@Slf4j
@Component
public class SecurityAuthenticationTokenFilter extends OncePerRequestFilter {

    @Autowired
    private JwtUtil jwtUtil;

    @Override
    protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
        String token = jwtUtil.getToken(httpServletRequest);
        //判断token是否有效
        if (StringUtils.hasText(token)) {
            //创建AuthenticationToken
            UsernamePasswordAuthenticationToken authentication = jwtUtil.getAuthentication(token);
            if (authentication != null) {
                SecurityContextHolder.getContext().setAuthentication(authentication);
            }
        } else {
            log.debug("token无效。");
        }
        filterChain.doFilter(httpServletRequest, httpServletResponse);
    }
}

9.配置项目中用到的bean


/**
 * @author : LCheng
 * @date : 2020-11-26 17:30
 * description : bean
 */
@Component
public class BeanConfig {

    @Bean
    public FilterRegistrationBean registration(SecurityAuthenticationTokenFilter filter) {
        FilterRegistrationBean registration = new FilterRegistrationBean(filter);
        registration.setEnabled(false);
        return registration;
    }

    /**
     * 使用BCrypt进行加密
     *
     * @return {@link PasswordEncoder}
     * @author LCheng
     * @date 2020/11/26 17:57
     */
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

10.配置UserDetailService

这里是重中之重,所有登录验证功能都是在这里完成的

/**
 * 用户登录操作
 *
 * @author lCheng
 */
@Component
@Slf4j
public class SecurityUserDetailService implements UserDetailsService {

    @Autowired
    private PasswordEncoder passwordEncoder;

    @Override
    public UserDetails loadUserByUsername(String accountName) throws UsernameNotFoundException {
        log.info("登陆用户名:" + accountName);
        //这里进行用户验证 根据系统自行填写逻辑代码
        //因为是测试,这里所有用户密码默认设置为123
        return new User(accountName, passwordEncoder.encode("123"), AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_USER"));
    }
}

11.配置SecurityConfig

这里是重中之重中之重,没有他什么也跑不起来,各块代码的注释都已表明


/**
 * @author : LCheng
 * @date : 2020-11-21 14:43
 * description : spring security配置
 */
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private SecurityAccessDeniedHandler securityAccessDeniedHandler;

    @Autowired
    private SecurityAuthenticationEntryPoint securityAuthenticationEntryPoint;

    @Autowired
    private UserDetailsService userDetailsService;

    @Autowired
    private PasswordEncoder passwordEncoder;

    @Autowired
    private SecurityAuthenticationTokenFilter securityAuthenticationTokenFilter;

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        super.configure(auth);
        auth
                // 设置UserDetailsService
                .userDetailsService(userDetailsService)
                // 使用BCrypt进行加密
                .passwordEncoder(passwordEncoder);
    }

    @Override
    protected void configure(HttpSecurity httpSecurity) throws Exception {

        ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry config = httpSecurity
                // 禁用CSRF
                .csrf().disable()
                .exceptionHandling()
                //认证失败处理
                .authenticationEntryPoint(securityAuthenticationEntryPoint)
                //无权限处理
                .accessDeniedHandler(securityAccessDeniedHandler)
                // 不创建session 使用token不需要session
                .and()
                .sessionManagement()
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .authorizeRequests()
                // 登录url不验证
                .antMatchers("/login").permitAll()
                // OPTIONS请求不验证
                .antMatchers(HttpMethod.OPTIONS, "/**").permitAll();

        // 剩下所有请求都需要认证
        config.anyRequest().authenticated();

        // 禁用缓存
        httpSecurity.headers().cacheControl();
        // 添加JWT filter
        httpSecurity
                .addFilterBefore(securityAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
    }

    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }
}

12.最后一步,配置一个测试控制器


/**
 * @author : LCheng
 * @date : 2020-11-26 17:44
 * description : 测试控制器
 */
@Slf4j
@RestController
public class TestController {

    @Autowired
    private AuthenticationManager authenticationManager;

    @Autowired
    private JwtUtil jwtUtil;

    @PostMapping(value = "/login")
    public String login(@RequestParam String username, @RequestParam String password) {
        //验证用户信息
        Authentication authentication = authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(username, password));
        SecurityContextHolder.getContext().setAuthentication(authentication);
        //生成token
        User user = (User) authentication.getPrincipal();
        return jwtUtil.generate(user);
    }

    @GetMapping(value = "/hello")
    public String hello() {
        return "hello word";
    }
}

这里需要注意一下,SecurityConfig中有把 /login 放行,所以要与控制器中进行对应。

13.最最后一步,实践检验真理的最后一步

  首先这里要安利一个工具,谷歌浏览器的一个插件: Talend API Tester ,本人喜欢用这个而不是postman。
  接下来真是进入测试环节:

  1. 首先在没有token下,去访问 /hello,由于没有认证会因此返回认证失败。
    在这里插入图片描述
  2. 去访问登录接口 /login ,输入错误的密码(密码默认设置为123,在SecurityUserDetailService 有注释请自行查看),由于密码错误因此依然返回认证失败,正常系统会在SecurityUserDetailService 进行密码验证,就可返回自定义的错误信息。
    在这里插入图片描述
  3. 去访问登录接口 /login ,输入正确的密码,成功返回了token
    在这里插入图片描述
  4. 反正返回的token去再去访问 /hello ,你会发现已经可以正常访问了!
    在这里插入图片描述

结束语

至此所有整合过程已经完毕。
源码已上传至gitee 地址为:https://gitee.com/lonecheng/springboot-springsecurity-jwt

  • 3
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 5
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值