⭐⭐⭐ Spring Boot 项目实战 ⭐⭐⭐ Spring Cloud 项目实战
《Dubbo 实现原理与源码解析 —— 精品合集》 《Netty 实现原理与源码解析 —— 精品合集》
《Spring 实现原理与源码解析 —— 精品合集》 《MyBatis 实现原理与源码解析 —— 精品合集》
《Spring MVC 实现原理与源码解析 —— 精品合集》 《数据库实体设计合集》
《Spring Boot 实现原理与源码解析 —— 精品合集》 《Java 面试题 + Java 学习指南》

摘要: 原创出处 阿Q说代码 「阿Q」欢迎转载,保留摘要,谢谢!


🙂🙂🙂关注**微信公众号:【芋道源码】**有福利:

  1. RocketMQ / MyCAT / Sharding-JDBC 所有源码分析文章列表
  2. RocketMQ / MyCAT / Sharding-JDBC 中文注释源码 GitHub 地址
  3. 您对于源码的疑问每条留言将得到认真回复。甚至不知道如何读源码也可以请教噢
  4. 新的源码解析文章实时收到通知。每周更新一篇左右
  5. 认真的源码交流微信群。

作为一名后端开发人员,权限这个名词应该算是特别熟悉的了。就算是java里的类也有 public、private 等权限之分。之前项目里一直使用shiro作为权限管理的框架。说实话,shiro的确挺强大的,但是它也有很多不好的地方。

Spring Security是Spring官方提供的安全框架,它具有能与Spring无缝整合、全面的权限控制、专门为web开发而设计等特点,比之shiro更加强大。今天我们就来聊聊如何使用Spring 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>

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-case: true

rsa:
key:
pubKeyFile: C:\Users\Desktop\jwt\id_key_rsa.pub
priKeyFile: C:\Users\Desktop\jwt\id_key_rsa

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,
`password` varchar(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,
`name` varchar(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;

引入依赖

<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>

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

其中用户名为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();
}
}

「拦截规则」

  • anyRequest:匹配所有请求路径
  • accessSpringEl表达式结果为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)));
}
}

认证流程

自定义认证过滤器

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();
}
}
}

流程

Security默认登录路径为/login,当我们调用该接口时,它会调用上边的attemptAuthentication方法;

所以我们要自定义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;
}
}

其中的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;
}
}

当认证通过之后会在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(), null, user.getAuthorities());
//将认证信息存到安全上下文中
SecurityContextHolder.getContext().setAuthentication(authResult);
chain.doFilter(request, response);
}
}
}
}

当我们访问时需要在header中携带token信息至于文中JWT生成tokenRSA生成公钥、私钥的部分,可在源码中查看。

项目源码地址

https://gitee.com/zhangxiaoQ/security-jwt

文章目录
  1. 1. 简介
  2. 2. 原理
  3. 3. 实战
    1. 3.1. 项目准备
    2. 3.2. 引入依赖
    3. 3.3. SecurityConfig类
      1. 3.3.1. 认证失败处理类
    4. 3.4. 认证流程
      1. 3.4.1. 自定义认证过滤器
      2. 3.4.2. 流程
      3. 3.4.3. 整体流程图如下
    5. 3.5. 鉴权流程
      1. 3.5.1. 自定义token过滤器
  4. 4. 项目源码地址