Spring Security + OAuth2.0 + JWT

文章目录

  • 前言
  • 一、Spring Security是什么?
    • 1.认证
      • 1.1基于Session认证
      • 1.2基于Token认证
    • 2.授权
    • 3.基于角色访问控制
  • 二、OAuth2.0是什么?
  • 三、JWT是什么?
  • 四、代码实现
    • 1.创建Spring Boot项目
    • 2.Spring Security
      • 2.1基本使用
      • 2.2自定义功能
        • 2.2.1集成数据库
      • 2.3通过注解权限控制
    • 3.OAuth2.0
      • 3.1基本使用
      • 3.2四种授权模式
        • 3.2.1 code码授权
        • 3.2.2 静默授权
        • 3.2.3 客户端授权
        • 3.2.4 密码授权
    • 4.JWT
      • 4.1 基本使用
        • 4.1.1 token过滤器
        • 4.1.2 WebSecurityConfig
        • 4.1.3 AuthorizationJwtServerConfig
        • 4.1.4 登出


前言

基于Spring Boot项目使用Spring Security+OAuth2.0+JWT搭建用户认证中心。


一、Spring Security是什么?

Spring Security是一个强大且高度可定制的身份验证和访问控制框架。它是用于保护基于Spring的应用程序的实际标准。Spring Security官网

1.认证

1.1基于Session认证

用户登录成功后,会创建一个session保存在服务器端,session id保存在cookie中

1.2基于Token认证

使用token作为唯一标识

2.授权

3.基于角色访问控制

二、OAuth2.0是什么?

OAuth 2.0 是授权的行业标准协议。OAuth 2.0 侧重于客户端开发人员的简单性,同时为 Web 应用程序、桌面应用程序、移动电话和客厅设备提供特定的授权流。该规范及其扩展正在IETF非授权工作组内制定。

三、JWT是什么?

JSON Web Token (JWT) 是一个开放标准(RFC 7519),它定义了一种紧凑且自成一体的方式,将各方之间安全传输信息作为 JSON 对象。此信息可以进行验证和信任,因为它是以数字方式签名的。JWTs 可以使用秘密(使用HMAC算法)或使用RSA或ECDSA的公钥/私钥对进行签名。

四、代码实现

1.创建Spring Boot项目

新建一个Spring Boot项目然后通过maven引入所需依赖就可以。需要注意的是不同版本的Spring Boot和依赖包可能会出现方法过时等问题。本demo的Spring Boot的版本是2.4.4。下面是所有依赖包。

<!-- 核心依赖 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter</artifactId>
</dependency>

<!-- 测试依赖 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
</dependency>

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

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

<!-- lombok -->
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <optional>true</optional>
</dependency>

<!-- mybatis-plus -->
<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-boot-starter</artifactId>
    <version>3.4.2</version>
</dependency>

<!-- mysql -->
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>8.0.22</version>
</dependency>

<!-- 数据源驱动包 -->
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid-spring-boot-starter</artifactId>
    <version>1.2.4</version>
</dependency>

<!-- hutool -->
<dependency>
    <groupId>cn.hutool</groupId>
    <artifactId>hutool-all</artifactId>
    <version>5.5.8</version>
</dependency>

<!-- jwt -->
<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-jwt</artifactId>
    <version>1.1.1.RELEASE</version>
</dependency>

<!-- jwt工具 -->
<dependency>
    <groupId>com.auth0</groupId>
    <artifactId>java-jwt</artifactId>
    <version>3.3.0</version>
</dependency>

<!--Spring boot Redis-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

<!-- 日志 -->
<dependency>
    <groupId>log4j</groupId>
    <artifactId>log4j</artifactId>
    <version>1.2.17</version>
</dependency>

<!-- oauth2.0 -->
<dependency>
    <groupId>org.springframework.security.oauth</groupId>
    <artifactId>spring-security-oauth2</artifactId>
    <version>2.3.6.RELEASE</version>
</dependency>

2.Spring Security

2.1基本使用

在引入Spring Security依赖之后启动项目,访问http://localhost:8080/login,Spring Security自带了一个登陆页面。如图
Spring Security + OAuth2.0 + JWT_第1张图片
这就说明Spring Security使用成功。用户名默认是user,密码在启动项目时会显示在控制台。
Spring Security + OAuth2.0 + JWT_第2张图片
接下来我们对Spring Security进行改造,实现一些我们自己的需求。

2.2自定义功能

2.2.1集成数据库

数据库连接省略
1.创建配置类

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true) // 开启方法级别安全
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Resource
    private UserDetailsServiceImpl userDetailsService;

    @Bean
    public BCryptPasswordEncoder bCryptPasswordEncoder() {
        return new BCryptPasswordEncoder();
    }
	
	/**
     * 一些简单的配置
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // 禁用session
        http.sessionManagement().disable();
        http.csrf().disable();
        http.formLogin()
                // 自定义登录成功后路径
                .defaultSuccessUrl("/hello")
                // 自定义登录路径
                .loginProcessingUrl("/doLogin");
        // 可以匿名访问
        http.authorizeRequests()
                .antMatchers("/doLogin").anonymous()
                .anyRequest().authenticated();
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        // 用户数据从数据库获取
        auth.userDetailsService(userDetailsService);
    }
}

2.自定义身份认证,创建一个类实现UserDetailsService

@Service
public class UserDetailsServiceImpl implements UserDetailsService {

    @Resource
    private BCryptPasswordEncoder bCryptPasswordEncoder;

    @Resource
    private UserMapper userMapper;

    @Override
    public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
    	/* 用户在登录时,会进入此方法(在配置类中进行配置),参数是用户名,这里使用了mybatisplus
    	 * 做了一个简单的通过用户名查询用户,springsecurity会自动对密码进行匹配
    	 */
        QueryWrapper<com.springboot.demo.security.entity.User> wrapper = new QueryWrapper<>();
        wrapper.eq("username", s);
        com.springboot.demo.security.entity.User sysUser = userMapper.selectOne(wrapper);
        String password = sysUser.getPassword();
        List<GrantedAuthority> userList = new ArrayList<>();
        // userList是权限集合,这里也是做一个简单的权限添加
        userList.add(new SimpleGrantedAuthority("add"));
        // springsecurity5.0后密码需要加密一次,不然会报错
        return new User("user", bCryptPasswordEncoder.encode(password), userList);
    }

3.访问登录页进行测试
Spring Security + OAuth2.0 + JWT_第3张图片
登录成功后跳转到自己配置的登录成功页面
Spring Security + OAuth2.0 + JWT_第4张图片

2.3通过注解权限控制

使用注解进行权限控制首先需要在配置类中加上注解@EnableGlobalMethodSecurity(prePostEnabled = true),表示开启方法级安全级别。有三种机制,这里使用@PreAuthorize注解,在上述登录中我们把add这个权限分配给了用户,所以登录后直接访问的hello没有权限限制可以直接访问

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

@PreAuthorize("hasAuthority('add')")
@GetMapping(value = "/add")
public String add() {
    return "add";
}

@PreAuthorize("hasAuthority('del')")
@GetMapping(value = "/del")
public String del() {
   return "del";
}

有add的权限也可以访问add
Spring Security + OAuth2.0 + JWT_第5张图片
访问del就会提示授权异常
Spring Security + OAuth2.0 + JWT_第6张图片
springsecurity的简单应用就大功告成。

3.OAuth2.0

3.1基本使用

首先引入所需maven依赖,然后我们创建一个配置类AuthorizationJwtServerConfig集成AuthorizationServerConfigurerAdapter,先使用redis做token存储。

@Configuration
@EnableAuthorizationServer
public class AuthorizationJwtServerConfig extends AuthorizationServerConfigurerAdapter {

    @Resource
    private RedisConnectionFactory redisConnectionFactory;

    @Bean
    public TokenStore tokenStore() {
        return new RedisTokenStore(redisConnectionFactory);
    }
	/**
     * 暴露授权服务
     * @param endpoints
     * @throws Exception
     */
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        endpoints.tokenStore(tokenStore());
    }
}

然后启动项目,我们可以在控制台中看到打印的内容,说明oauth2.0可以使用。在这里插入图片描述

3.2四种授权模式

3.2.1 code码授权

通过code码换取token。
在AuthorizationJwtServerConfig配置类添加配置。

@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
    clients.inMemory()
            // 第三方应用客户端id,相当于账号,可自定义
            .withClient("web")
            // 第三方应用密码,需要加密,相当于密码,可自定义
            .secret(new BCryptPasswordEncoder().encode("web"))
            // 第三方作用域,自定义
            .scopes("read")
            // 授权类型,使用code码
            .authorizedGrantTypes("authorization_code")
            // 有效时间
            .accessTokenValiditySeconds(7200)
            // 重定向url,必须是公网地址,必须是https
            .redirectUris("https://www.baidu.com");
}

重启项目后我们在浏览器上访问http://localhost:8080/oauth/authorize?response_type=code&scope=read&client_id=web&redirect_uri=https://www.baidu.com
response_type参数是授权类型code是code码授权,scope作用域对应代码配置中的作用域值,client_id也是对应代码中的配置,redirect_uri同理。
回车之后会跳转到登录页面,然后输入账号密码再次登录,会跳转到授权页面
Spring Security + OAuth2.0 + JWT_第7张图片
确认授权后会跳转到我们配置的重定向地址,并且获得了code码
Spring Security + OAuth2.0 + JWT_第8张图片
有了code码之后我们就要用code码去换取token,使用工具postman发送一个post请求
Spring Security + OAuth2.0 + JWT_第9张图片
除此之外还需要配置Authorization,类型选择Basic Auth,账号密码是代码中配置的
Spring Security + OAuth2.0 + JWT_第10张图片
发送请求之后就会获得token了
Spring Security + OAuth2.0 + JWT_第11张图片

3.2.2 静默授权

直接获取token,代码跟code码授权没有太大区别,类型换成implicit静默授权就可以了

@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
    clients.inMemory()
            // 第三方应用客户端id
            .withClient("app")
            // 第三方应用密码,需要加密
            .secret(new BCryptPasswordEncoder().encode("app"))
            // 第三方作用域
            .scopes("read")
            // 授权类型
            .authorizedGrantTypes("implicit")
            .accessTokenValiditySeconds(7200)
            .redirectUris("https://www.baidu.com");
}

重启项目,浏览器访问http://localhost:8080/oauth/authorize?response_type=token&scope=read&client_id=app&redirect_uri=https://www.baidu.com
注意这里的response_type的值是token,直接访问也会跳转到登录页面,登录成功后进行授权
Spring Security + OAuth2.0 + JWT_第12张图片
授权成功后,token会显示在浏览器上
Spring Security + OAuth2.0 + JWT_第13张图片

3.2.3 客户端授权
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
    clients.inMemory()
            // 第三方应用客户端id
            .withClient("client")
            // 第三方应用密码,需要加密
            .secret(new BCryptPasswordEncoder().encode("client"))
            // 第三方作用域
            .scopes("read")
            // 授权类型
            .authorizedGrantTypes("client_credentials")
            .accessTokenValiditySeconds(7200)
            .redirectUris("https://www.baidu.com");
}

使用postman发送post请求获取token,只需grant_type一个参数
Spring Security + OAuth2.0 + JWT_第14张图片
Spring Security + OAuth2.0 + JWT_第15张图片

3.2.4 密码授权

首先更改WebSecurityConfig配置类,增加一个认证管理器

/**
 * 用密码模式授权认证管理器
 * @return
 * @throws
 */
@Override
@Bean
public AuthenticationManager authenticationManager() throws Exception {
    return super.authenticationManager();
}

AuthorizationJwtServerConfig类中把AuthenticationManager注入进来,暴露授权服务中加入认证管理器

@Resource
private AuthenticationManager authenticationManager;

@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
    clients.inMemory()
            // 第三方应用客户端id
            .withClient("qq")
            // 第三方应用密码,需要加密
            .secret(new BCryptPasswordEncoder().encode("qq"))
            // 第三方作用域
            .scopes("read")
            // 授权类型
            .authorizedGrantTypes("password")
            .accessTokenValiditySeconds(7200)
            .redirectUris("https://www.baidu.com");
}

/**
 * 暴露授权服务
 * @param endpoints
 * @throws Exception
 */
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
    endpoints.tokenStore(tokenStore()).authenticationManager(authenticationManager);
}

重启项目后使用postman来测试,发送post请求
Spring Security + OAuth2.0 + JWT_第16张图片
Authorization也需要配置
Spring Security + OAuth2.0 + JWT_第17张图片
成功后直接获取token
Spring Security + OAuth2.0 + JWT_第18张图片

4.JWT

4.1 基本使用

使用jwt生成token,并做token的验证

4.1.1 token过滤器
@Configuration
public class JwtTokenFilter extends OncePerRequestFilter {
    @Override
    protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
        String path = httpServletRequest.getRequestURI();
        String method = httpServletRequest.getMethod();
        // 对于登录直接放行
        if ("/login".equals(path) && "POST".equals(method)) {
            filterChain.doFilter(httpServletRequest, httpServletResponse);
            return;
        }
        // 获取token并验证
        String authorization = httpServletRequest.getHeader("Authorization");
        if (!StrUtil.hasBlank(authorization)) {
            String jwt = authorization.replaceAll("bearer ", "");
            // 创建一个token解析器(test作为jwt生成token的签名是自定义的,一般是作为配置固定值)
            JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256("test")).build();
            DecodedJWT decodedJwt;
            try {
                decodedJwt = jwtVerifier.verify(jwt);
            } catch (Exception e) {
                httpServletResponse.getWriter().write("token验证失败");
                return;
            }
            // 获取用户名,密码,角色权限
            String username = decodedJwt.getClaim("username").asString();
            String password = decodedJwt.getClaim("password").asString();
            List<String> roles = decodedJwt.getClaim("role").asList(String.class);
            List<SimpleGrantedAuthority> roleList = new ArrayList<>();
            roles.forEach(role -> {
                roleList.add(new SimpleGrantedAuthority(role));
            });
            UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken =
                    new UsernamePasswordAuthenticationToken(username, password, roleList);      
            SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);
            filterChain.doFilter(httpServletRequest, httpServletResponse);
            return;
        }
        httpServletResponse.getWriter().write("token验证失败");
    }
}
4.1.2 WebSecurityConfig
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Resource
    private UserDetailsServiceImpl userDetailsService;

    @Resource
    private JwtTokenFilter jwtTokenFilter;

    @Resource
    private RedisTemplate<String, Object> redisTemplate;

    @Bean
    public BCryptPasswordEncoder bCryptPasswordEncoder() {
        return new BCryptPasswordEncoder();
    }

    /**
     * 一些简单的配置
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // 登录之前验证token
        http.addFilterBefore(jwtTokenFilter, UsernamePasswordAuthenticationFilter.class);
        // 禁用session
        http.sessionManagement().disable();
        http.csrf().disable();
        http.formLogin()
                // 登录成功处理器
                .successHandler(authenticationSuccessHandler())
                // 登录失败处理器
                .failureHandler(authenticationFailureHandler());
        // 除了登录可以匿名访问
        http.authorizeRequests()
                .antMatchers("/login").anonymous()
                .anyRequest().authenticated();
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        // 用户数据从数据库获取
        auth.userDetailsService(userDetailsService);
    }
	
	/**
	 * 登录成功处理器
	 */
    @Bean
    public AuthenticationSuccessHandler authenticationSuccessHandler() {
        return ((httpServletRequest, httpServletResponse, authentication) -> {
            httpServletResponse.setContentType("application/json;charset=utf-8");
            User user = (User)authentication.getPrincipal();
            // 用户名
            String username = user.getUsername();
            // 密码
            String password = user.getPassword();
            // 权限
            Collection<GrantedAuthority> grantedAuthorities =  user.getAuthorities();
            List<String> roleList = new ArrayList<>();
            grantedAuthorities.forEach(grantedAuthority -> {
                roleList.add(grantedAuthority.getAuthority());
            });
            String[] roles = new String[roleList.size()];
            // 用jwt生成token
            HashMap<String, Object> headMap = new HashMap<>(16);
            // 使用的算法
            headMap.put("alg", "HS256");
            headMap.put("typ", "JWT");
            Date nowDate = new Date();
            // 过期时间可以自定义
            Date expDate = new Date(nowDate.getTime() + 2 * 60 * 60 * 1000);
            String jwt = JWT.create().withHeader(headMap)
                    .withIssuedAt(nowDate)
                    .withExpiresAt(expDate)
                    // 主题,自定义
                    .withSubject("demo")
                    .withClaim("username", username)
                    .withClaim("password", password)
                    .withArrayClaim("role", roleList.toArray(roles))
                    // 签名,自定义,同一个项目中签名是唯一
                    .sign(Algorithm.HMAC256("test"));
            // 保存token到redis
            redisTemplate.opsForValue().set("token:" + jwt, user, 7200);
            // 返回token
            HashMap<String, Object> hashMap = new HashMap<>(16);
            hashMap.put("username", username);
            hashMap.put("create_time", nowDate);
            hashMap.put("expires_time", expDate);
            hashMap.put("access_token", jwt);
            hashMap.put("type", "bearer");
            ObjectMapper objectMapper = new ObjectMapper();
            String s = objectMapper.writeValueAsString(hashMap);
            PrintWriter printWriter = httpServletResponse.getWriter();
            printWriter.write(s);
            printWriter.flush();
            printWriter.close();
        });
    }

	/**
	 * 登录失败处理器
	 */
    @Bean
    public AuthenticationFailureHandler authenticationFailureHandler() {
        return ((httpServletRequest, httpServletResponse, e) -> {
            HashMap<String, Object> hashMap = new HashMap<>(16);
            hashMap.put("error", e);
            hashMap.put("message", "登录失败");
            ObjectMapper objectMapper = new ObjectMapper();
            String s = objectMapper.writeValueAsString(hashMap);
            PrintWriter printWriter = httpServletResponse.getWriter();
            printWriter.write(s);
            printWriter.flush();
            printWriter.close();
        });
    }
}
4.1.3 AuthorizationJwtServerConfig
@Configuration
@EnableAuthorizationServer
public class AuthorizationJwtServerConfig extends AuthorizationServerConfigurerAdapter {

    @Bean
    public TokenStore tokenStore() {
        return new JwtTokenStore(jwtAccessTokenConverter());
    }

    /**
     * jwt token转换器
     * @return
     */
    @Bean
    public JwtAccessTokenConverter jwtAccessTokenConverter() {
        JwtAccessTokenConverter jwtAccessTokenConverter = new JwtAccessTokenConverter();
        // 签名
        jwtAccessTokenConverter.setSigningKey("test");
        return jwtAccessTokenConverter;
    }

    /**
     * 暴露授权服务
     * @param endpoints
     * @throws Exception
     */
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        endpoints.accessTokenConverter(jwtAccessTokenConverter());
    }
}

然后启动项目,使用postman测试
登录获取token
Spring Security + OAuth2.0 + JWT_第19张图片
使用token访问接口
Spring Security + OAuth2.0 + JWT_第20张图片

4.1.4 登出

登出就相当于是删除redis缓存的token

@PostMapping(value = "/doLogout")
    public Object logout() {
        ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = requestAttributes.getRequest();
        String head = request.getHeader("Authorization");
        if (!StrUtil.isBlank(head)) {
            String jwt = head.replaceAll("bearer ", "");
            if (!StrUtil.hasBlank(jwt)) {
                redisTemplate.delete("token:" + jwt);
                return "登出成功";
            }
        }
        return "登出失败";
    }

你可能感兴趣的:(Java)