实战篇:Security+JWT组合拳

开发 后端
Web应用的安全性包括用户认证和用户授权两个部分,而Spring Security(以下简称Security)基于Spring框架,正好可以完整解决该问题。它的真正强大之处在于它可以轻松扩展以满足自定义要求。

[[408789]]

Good morning, everyone!

之前我们已经说过用Shiro和JWT来实现身份认证和用户授权,今天我们再来说一下「Security和JWT」的组合拳。

简介

先赘述一下身份认证和用户授权:

用户认证(Authentication):系统通过校验用户提供的用户名和密码来验证该用户是否为系统中的合法主体,即是否可以访问该系统;

用户授权(Authorization):系统为用户分配不同的角色,以获取对应的权限,即验证该用户是否有权限执行该操作;

Web应用的安全性包括用户认证和用户授权两个部分,而Spring Security(以下简称Security)基于Spring框架,正好可以完整解决该问题。

它的真正强大之处在于它可以轻松扩展以满足自定义要求。

原理

Security可以看做是由一组filter过滤器链组成的权限认证。它的整个工作流程如下所示:

图中绿色认证方式是可以配置的,橘黄色和蓝色的位置不可更改:

  • FilterSecurityInterceptor:最后的过滤器,它会决定当前的请求可不可以访问Controller
  • ExceptionTranslationFilter:异常过滤器,接收到异常消息时会引导用户进行认证;

实战

项目准备

我们使用Spring Boot框架来集成。

1.pom文件引入的依赖

<dependency> 
    <groupId>org.springframework.boot</groupId> 
    <artifactId>spring-boot-starter</artifactId> 
</dependency> 
 
<dependency> 
    <groupId>org.springframework.boot</groupId> 
    <artifactId>spring-boot-starter-web</artifactId> 
    <exclusions> 
        <exclusion> 
            <groupId>org.springframework.boot</groupId> 
            <artifactId>spring-boot-starter-tomcat</artifactId> 
        </exclusion> 
    </exclusions> 
</dependency> 
 
<dependency> 
    <groupId>org.springframework.boot</groupId> 
    <artifactId>spring-boot-starter-undertow</artifactId> 
</dependency> 
 
<dependency> 
    <groupId>mysql</groupId> 
    <artifactId>mysql-connector-java</artifactId> 
</dependency> 
 
<dependency> 
    <groupId>com.baomidou</groupId> 
    <artifactId>mybatis-plus-boot-starter</artifactId> 
    <version>3.4.0</version> 
</dependency> 
 
<dependency> 
    <groupId>org.projectlombok</groupId> 
    <artifactId>lombok</artifactId> 
</dependency> 
 
<!-- 阿里JSON解析器 --> 
<dependency> 
    <groupId>com.alibaba</groupId> 
    <artifactId>fastjson</artifactId> 
    <version>1.2.74</version> 
</dependency> 
 
<dependency> 
    <groupId>joda-time</groupId> 
    <artifactId>joda-time</artifactId> 
    <version>2.10.6</version> 
</dependency> 
 
<dependency> 
    <groupId>org.springframework.boot</groupId> 
    <artifactId>spring-boot-starter-test</artifactId> 
</dependency> 
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.
  • 41.
  • 42.
  • 43.
  • 44.
  • 45.
  • 46.
  • 47.
  • 48.
  • 49.
  • 50.
  • 51.
  • 52.
  • 53.
  • 54.

 

2.application.yml配置

spring: 
  application: 
    name: securityjwt 
  datasource: 
    driver-class-name: com.mysql.cj.jdbc.Driver 
    url: jdbc:mysql://127.0.0.1:3306/cheetah?characterEncoding=utf-8&useSSL=false&serverTimezone=UTC 
    username: root 
    password: 123456 
 
server: 
  port: 8080 
 
mybatis: 
  mapper-locations: classpath:mapper/*.xml 
  type-aliases-package: com.itcheetah.securityjwt.entity 
  configuration: 
    map-underscore-to-camel-casetrue 
 
rsa: 
  key
    pubKeyFile: C:\Users\Desktop\jwt\id_key_rsa.pub 
    priKeyFile: C:\Users\Desktop\jwt\id_key_rsa 
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.

3.SQL文件

/** 
* sys_user_info 
**/ 
 
SET NAMES utf8mb4; 
SET FOREIGN_KEY_CHECKS = 0; 
 
-- ---------------------------- 
-- Table structure for sys_user_info 
-- ---------------------------- 
DROP TABLE IF EXISTS `sys_user_info`; 
CREATE TABLE `sys_user_info`  ( 
  `id` bigint(20) NOT NULL AUTO_INCREMENT, 
  `username` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL
  `passwordvarchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL
  PRIMARY KEY (`id`) USING BTREE 
) ENGINE = InnoDB AUTO_INCREMENT = 3 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic
 
SET FOREIGN_KEY_CHECKS = 1; 
 
 
/** 
* product_info 
**/ 
 
SET NAMES utf8mb4; 
SET FOREIGN_KEY_CHECKS = 0; 
 
-- ---------------------------- 
-- Table structure for product_info 
-- ---------------------------- 
DROP TABLE IF EXISTS `product_info`; 
CREATE TABLE `product_info`  ( 
  `id` bigint(20) NOT NULL AUTO_INCREMENT, 
  `namevarchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL
  `price` decimal(10, 4) NULL DEFAULT NULL
  `create_date` datetime(0) NULL DEFAULT NULL
  `update_date` datetime(0) NULL DEFAULT NULL
  PRIMARY KEY (`id`) USING BTREE 
) ENGINE = InnoDB AUTO_INCREMENT = 4 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic
 
SET FOREIGN_KEY_CHECKS = 1; 
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.
  • 41.
  • 42.

引入依赖

<dependency> 
    <groupId>org.springframework.boot</groupId> 
    <artifactId>spring-boot-starter-security</artifactId> 
</dependency> 
 
<!--Token生成与解析--> 
<dependency> 
    <groupId>io.jsonwebtoken</groupId> 
    <artifactId>jjwt</artifactId> 
    <version>0.9.1</version> 
</dependency> 
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.

 

引入之后启动项目,会有如图所示:

其中用户名为user,密码为上图中的字符串。

SecurityConfig类

//开启全局方法安全性 
@EnableGlobalMethodSecurity(prePostEnabled=true, securedEnabled=true
public class SecurityConfig extends WebSecurityConfigurerAdapter { 
 
    //认证失败处理类 
    @Autowired 
    private AuthenticationEntryPointImpl unauthorizedHandler; 
 
    //提供公钥私钥的配置类 
    @Autowired 
    private RsaKeyProperties prop; 
 
    @Autowired 
    private UserInfoService userInfoService; 
     
    @Override 
    protected void configure(HttpSecurity httpSecurity) throws Exception { 
        httpSecurity 
                // CSRF禁用,因为不使用session 
                .csrf().disable() 
                // 认证失败处理类 
                .exceptionHandling().authenticationEntryPoint(unauthorizedHandler).and() 
                // 基于token,所以不需要session 
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and() 
                // 过滤请求 
                .authorizeRequests() 
                .antMatchers( 
                        HttpMethod.GET, 
                        "/*.html"
                        "/**/*.html"
                        "/**/*.css"
                        "/**/*.js" 
                ).permitAll() 
                // 除上面外的所有请求全部需要鉴权认证 
                .anyRequest().authenticated() 
                .and() 
                .headers().frameOptions().disable(); 
        // 添加JWT filter 
        httpSecurity.addFilter(new TokenLoginFilter(super.authenticationManager(), prop)) 
                .addFilter(new TokenVerifyFilter(super.authenticationManager(), prop)); 
    } 
 
    //指定认证对象的来源 
    public void configure(AuthenticationManagerBuilder auth) throws Exception { 
         
        auth.userDetailsService(userInfoService) 
        //从前端传递过来的密码就会被加密,所以从数据库 
        //查询到的密码必须是经过加密的,而这个过程都是 
        //在用户注册的时候进行加密的。 
        .passwordEncoder(passwordEncoder()); 
    } 
 
    //密码加密 
    @Bean 
    public BCryptPasswordEncoder passwordEncoder(){ 
        return new BCryptPasswordEncoder(); 
    } 

  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.
  • 41.
  • 42.
  • 43.
  • 44.
  • 45.
  • 46.
  • 47.
  • 48.
  • 49.
  • 50.
  • 51.
  • 52.
  • 53.
  • 54.
  • 55.
  • 56.
  • 57.
  • 58.

「拦截规则」

  • anyRequest:匹配所有请求路径
  • access:SpringEl表达式结果为true时可以访问
  • anonymous:匿名可以访问
  • `denyAll:用户不能访问
  • fullyAuthenticated:用户完全认证可以访问(非remember-me下自动登录)
  • hasAnyAuthority:如果有参数,参数表示权限,则其中任何一个权限可以访问
  • hasAnyRole:如果有参数,参数表示角色,则其中任何一个角色可以访问
  • hasAuthority:如果有参数,参数表示权限,则其权限可以访问
  • hasIpAddress:如果有参数,参数表示IP地址,如果用户IP和参数匹配,则可以访问
  • hasRole:如果有参数,参数表示角色,则其角色可以访问
  • permitAll:用户可以任意访问
  • rememberMe:允许通过remember-me登录的用户访问
  • authenticated:用户登录后可访问

认证失败处理类

/** 
 *  返回未授权 
 */ 
@Component 
public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint, Serializable { 
 
    private static final long serialVersionUID = -8970718410437077606L; 
 
    @Override 
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) 
            throws IOException { 
        int code = HttpStatus.UNAUTHORIZED; 
        String msg = "认证失败,无法访问系统资源,请先登陆"
        ServletUtils.renderString(response, JSON.toJSONString(AjaxResult.error(code, msg))); 
    } 

  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.

认证流程

自定义认证过滤器

public class TokenLoginFilter extends UsernamePasswordAuthenticationFilter { 
 
    private AuthenticationManager authenticationManager; 
 
    private RsaKeyProperties prop; 
 
    public TokenLoginFilter(AuthenticationManager authenticationManager, RsaKeyProperties prop) { 
        this.authenticationManager = authenticationManager; 
        this.prop = prop; 
    } 
 
    /** 
     * @author cheetah 
     * @description 登陆验证 
     * @date 2021/6/28 16:17 
     * @Param [request, response] 
     * @return org.springframework.security.core.Authentication 
     **/ 
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException { 
        try { 
            UserPojo sysUser = new ObjectMapper().readValue(request.getInputStream(), UserPojo.class); 
            UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(sysUser.getUsername(), sysUser.getPassword()); 
            return authenticationManager.authenticate(authRequest); 
        }catch (Exception e){ 
            try { 
                response.setContentType("application/json;charset=utf-8"); 
                response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); 
                PrintWriter out = response.getWriter(); 
                Map resultMap = new HashMap(); 
                resultMap.put("code", HttpServletResponse.SC_UNAUTHORIZED); 
                resultMap.put("msg""用户名或密码错误!"); 
                out.write(new ObjectMapper().writeValueAsString(resultMap)); 
                out.flush(); 
                out.close(); 
            }catch (Exception outEx){ 
                outEx.printStackTrace(); 
            } 
            throw new RuntimeException(e); 
        } 
    } 
 
 
    /** 
     * @author cheetah 
     * @description 登陆成功回调 
     * @date 2021/6/28 16:17 
     * @Param [request, response, chain, authResult] 
     * @return void 
     **/ 
    public void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException { 
        UserPojo user = new UserPojo(); 
        user.setUsername(authResult.getName()); 
        user.setRoles((List<RolePojo>)authResult.getAuthorities()); 
        //通过私钥进行加密:token有效期一天 
        String token = JwtUtils.generateTokenExpireInMinutes(user, prop.getPrivateKey(), 24 * 60); 
        response.addHeader("Authorization""Bearer "+token); 
        try { 
            response.setContentType("application/json;charset=utf-8"); 
            response.setStatus(HttpServletResponse.SC_OK); 
            PrintWriter out = response.getWriter(); 
            Map resultMap = new HashMap(); 
            resultMap.put("code", HttpServletResponse.SC_OK); 
            resultMap.put("msg""认证通过!"); 
            resultMap.put("token", token); 
            out.write(new ObjectMapper().writeValueAsString(resultMap)); 
            out.flush(); 
            out.close(); 
        }catch (Exception outEx){ 
            outEx.printStackTrace(); 
        } 
    } 

  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.
  • 41.
  • 42.
  • 43.
  • 44.
  • 45.
  • 46.
  • 47.
  • 48.
  • 49.
  • 50.
  • 51.
  • 52.
  • 53.
  • 54.
  • 55.
  • 56.
  • 57.
  • 58.
  • 59.
  • 60.
  • 61.
  • 62.
  • 63.
  • 64.
  • 65.
  • 66.
  • 67.
  • 68.
  • 69.
  • 70.
  • 71.
  • 72.

流程

Security默认登录路径为/login,当我们调用该接口时,它会调用上边的attemptAuthentication方法;图片图片图片图片所以我们要自定义UserInfoService继承UserDetailsService实现loadUserByUsername方法;

所以我们要自定义UserInfoService继承UserDetailsService实现loadUserByUsername方法;

public interface UserInfoService extends UserDetailsService { 
 

 
@Service 
@Transactional 
public class UserInfoServiceImpl implements UserInfoService { 
 
    @Autowired 
    private SysUserInfoMapper userInfoMapper; 
 
    @Override 
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { 
        UserPojo user = userInfoMapper.queryByUserName(username); 
        return user
    } 

  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.

其中的loadUserByUsername返回的是UserDetails类型,所以UserPojo继承UserDetails类

@Data 
public class UserPojo implements UserDetails { 
 
    private Integer id; 
 
    private String username; 
 
    private String password
 
    private Integer status; 
 
    private List<RolePojo> roles; 
 
    @JsonIgnore 
    @Override 
    public Collection<? extends GrantedAuthority> getAuthorities() { 
        //理想型返回 admin 权限,可自已处理这块 
        List<SimpleGrantedAuthority> auth = new ArrayList<>(); 
        auth.add(new SimpleGrantedAuthority("ADMIN")); 
        return auth; 
    } 
 
    @Override 
    public String getPassword() { 
        return this.password
    } 
 
    @Override 
    public String getUsername() { 
        return this.username; 
    } 
 
    /** 
     * 账户是否过期 
     **/ 
    @JsonIgnore 
    @Override 
    public boolean isAccountNonExpired() { 
        return true
    } 
 
    /** 
     * 是否禁用 
     */ 
    @JsonIgnore 
    @Override 
    public boolean isAccountNonLocked() { 
        return true
    } 
 
    /** 
     * 密码是否过期 
     */ 
    @JsonIgnore 
    @Override 
    public boolean isCredentialsNonExpired() { 
        return true
    } 
 
    /** 
     * 是否启用 
     */ 
    @JsonIgnore 
    @Override 
    public boolean isEnabled() { 
        return true
    } 

  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.
  • 41.
  • 42.
  • 43.
  • 44.
  • 45.
  • 46.
  • 47.
  • 48.
  • 49.
  • 50.
  • 51.
  • 52.
  • 53.
  • 54.
  • 55.
  • 56.
  • 57.
  • 58.
  • 59.
  • 60.
  • 61.
  • 62.
  • 63.
  • 64.
  • 65.
  • 66.
  • 67.
  • 68.

当认证通过之后会在SecurityContext中设置Authentication对象,回调调用successfulAuthentication方法返回token信息,

整体流程图如下

鉴权流程

自定义token过滤器

public class TokenVerifyFilter extends BasicAuthenticationFilter { 
    private RsaKeyProperties prop; 
 
    public TokenVerifyFilter(AuthenticationManager authenticationManager, RsaKeyProperties prop) { 
        super(authenticationManager); 
        this.prop = prop; 
    } 
 
    public void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException { 
        String header = request.getHeader("Authorization"); 
        if (header == null || !header.startsWith("Bearer ")) { 
            //如果携带错误的token,则给用户提示请登录! 
            chain.doFilter(request, response); 
        } else { 
            //如果携带了正确格式的token要先得到token 
            String token = header.replace("Bearer """); 
            //通过公钥进行解密:验证tken是否正确 
            Payload<UserPojo> payload = JwtUtils.getInfoFromToken(token, prop.getPublicKey(), UserPojo.class); 
            UserPojo user = payload.getUserInfo(); 
            if(user!=null){ 
                UsernamePasswordAuthenticationToken authResult = new UsernamePasswordAuthenticationToken(user.getUsername(), nulluser.getAuthorities()); 
                //将认证信息存到安全上下文中 
                SecurityContextHolder.getContext().setAuthentication(authResult); 
                chain.doFilter(request, response); 
            } 
        } 
    } 

  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.

当我们访问时需要在header中携带token信息

本文转载自微信公众号「阿Q说代码」,可以通过以下二维码关注。转载本文请联系阿Q说代码公众号。

 

责任编辑:武晓燕 来源: 阿Q说代码
相关推荐

2009-06-15 16:05:30

设计AnnotatioJava

2019-05-21 14:33:01

2021-07-05 08:41:49

RedisGEO系统

2017-11-08 13:31:34

分层架构代码DDD

2011-10-31 09:26:07

惠普转型云计算

2014-07-06 14:05:22

IBMWebSphere数字经济

2018-05-08 18:26:49

数据库MySQL性能

2021-04-29 09:40:32

测试IDEAirtest

2011-09-26 11:28:51

信息防泄漏溢信科技

2023-02-23 10:03:57

2010-11-09 10:03:26

2016-08-31 09:19:57

2011-09-18 14:27:58

2015-10-20 19:08:28

华三通信

2016-12-09 13:45:21

RNN大数据深度学习

2021-09-08 09:48:39

数据库工具技术

2023-02-23 10:11:15

OKR项目管理

2023-11-21 08:25:09

2021-09-09 08:55:50

Python项目验证码

2021-07-06 08:41:54

RedisGeo 类型数据类型
点赞
收藏

51CTO技术栈公众号