Ensine-o Shiro a integrar o JWT para obter autenticação de login!

Estou participando do "Programa Nuggets · Sailing".

1. Tecnologia usada

  • SpringBoot
  • Mybatis-plus
  • Shiro
  • JWT
  • Redis

Nota: O código completo será fornecido no final

2. Pré-conhecimento

Shiro:

Shiro é uma estrutura de segurança de código aberto baseada em Java.

Na arquitetura central de Shiro, o Sujeito é o usuário que acessa o sistema. SecurityManager é um gerenciador de segurança responsável pela autenticação e autorização do usuário, equivalente ao irmão mais velho de Shiro.

O Realm é equivalente a uma fonte de dados, e a autenticação e autorização do usuário são realizadas nos métodos do Realm.

A criptografia é usada para gerenciar senhas de usuários e realizar operações de criptografia e descriptografia em senhas.

JWT:

O nome completo do JWT é json web token, que é, na verdade, uma string de strings gerada por "esfregar" as informações de login, tempo de expiração e algoritmo de criptografia do usuário. Essa string também é chamada de token. Claro, você também pode chamá-la um token.

Se um usuário deseja acessar o sistema, o cabeçalho da solicitação deve conter o token gerado pelo JWT. O sistema pode ser acessado somente se a verificação do token for aprovada, caso contrário, uma exceção será lançada.

3. Explicação do processo

1. O usuário clica para se cadastrar e o sistema criptografa a senha e a armazena no banco de dados.

2. Login do usuário

É principalmente para verificar a senha da conta e gerar um token.

image.png

3. Acesse recursos

Na verdade, no sistema de integração JWT da Shiro, a chave é usar o filtro JwtFilter para verificar se o cabeçalho da solicitação contém um token.Se houver um token, ele será entregue a um Realm personalizado.

Em seguida, verifique se o token está correto ou expirou no método de autenticação do Realm.

4. Inicialize o projeto SpringBoot

1. Crie uma nova tabela de banco de dados

CREATE TABLE `t_user` (
  `id` bigint NOT NULL COMMENT 'id',
  `name` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '姓名',
  `age` int DEFAULT NULL COMMENT '年龄',
  `sex` tinyint DEFAULT '0' COMMENT '性别:0-女 1-男',
  `username` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '账号',
  `password` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '密码',
  `created_date` datetime DEFAULT NULL COMMENT '创建时间',
  `updated_date` datetime DEFAULT NULL COMMENT '修改时间',
  `is_deleted` int DEFAULT '0' COMMENT '删除标识',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci ROW_FORMAT=DYNAMIC;
复制代码

2. Crie um novo projeto SpringBoot

Adicionar dependências:

<dependencies>
    <!--web-->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <!--引入shiro整合Springboot依赖-->
    <dependency>
        <groupId>org.apache.shiro</groupId>
        <artifactId>shiro-spring-boot-starter</artifactId>
        <version>1.5.3</version>
    </dependency>
    <!--引入jwt-->
    <dependency>
        <groupId>com.auth0</groupId>
        <artifactId>java-jwt</artifactId>
        <version>3.10.3</version>
    </dependency>
    <!--redis-->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>
    <!--myql-->
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <scope>runtime</scope>
    </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.0</version>
    </dependency>
    <!--逆向工程-->
    <dependency>
        <groupId>com.baomidou</groupId>
        <artifactId>mybatis-plus-generator</artifactId>
        <version>3.5.1</version>
    </dependency>
    <!--freemarker-->
    <dependency>
        <groupId>org.freemarker</groupId>
        <artifactId>freemarker</artifactId>
    </dependency>
    <!--druid-->
    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>druid</artifactId>
        <version>1.1.6</version>
    </dependency>
    <!--hutool-->
    <dependency>
        <groupId>cn.hutool</groupId>
        <artifactId>hutool-all</artifactId>
        <version>5.5.7</version>
    </dependency>
    <!--test-->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
</dependencies>
复制代码

Modifique o arquivo de configuração:

Defina principalmente a fonte de dados, mybatis-plus, redis e chaves jwt.

server:
  port: 8081
  servlet:
    context-path: /shiro_jwt
spring:
  # 数据源
  datasource:
    url: jdbc:mysql://localhost:3306/shiro_jwt?allowPublicKeyRetrieval=true&useSSL=false
    username: root
    password: 12345678
    driver-class-name: com.mysql.cj.jdbc.Driver
    type: com.alibaba.druid.pool.DruidDataSource
  # Redis
  redis:
    host: 172.16.255.3
    port: 6379
    database: 0
    password: 123456
# MybatisPlus
mybatis-plus:
  global-config:
    db-config:
      field-strategy: IGNORED
      column-underline: true
      logic-delete-field: isDeleted # 全局逻辑删除的实体字段名
      logic-delete-value: 1 # 逻辑已删除值(默认为 1)
      logic-not-delete-value: 0 # 逻辑未删除值(默认为 0)
      db-type: mysql
      id-type: assign_id
  mapper-locations: classpath*:/mapper/**Mapper.xml
  type-aliases-package: com.zhifou.entity
  configuration:
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
#jwt
jwt:
  secret: zhifou_secret!
复制代码

Geração de código:

Use o gerador de código Mybatis-plus para gerar arquivos de entidade, controlador, serviço, dao, mapeador

Configure o Redis (não é o ponto):

Ferramentas de criptografia e descriptografia:

O método de ferramenta de criptografia e descriptografia do hutool é usado principalmente aqui.

Tratamento de exceção global:

Retorne o resultado uniformemente:

5. Configurar JWT

1.JWT 工具类

主要用来生成 token、校验 token

2.JWTFilter

在 shiro 中,shiroFilter 用来拦截所有请求。

但是 shiro 要和 jwt 整合,所以要使用自定义的过滤器 JwtFilter。

JwtFilter 的主要作用就是拦截请求,判断请求头中书否携带 token。如果携带,就交给 Realm 处理。

@Component
@Slf4j
public class JwtFilter extends BasicHttpAuthenticationFilter {
    private String errorMsg;

    // 过滤器拦截请求的入口方法
    @Override
    protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
        // 判断请求头是否带上“Token”
        HttpServletRequest httpServletRequest = (HttpServletRequest) request;
        String token = httpServletRequest.getHeader("Authorization");
        // 游客访问电商平台首页可以不用携带 token
        if (StringUtils.isEmpty(token)) {
            return true;
        }
        try {
            // 交给 myRealm
            SecurityUtils.getSubject().login(new JwtToken(token));
            return true;
        } catch (Exception e) {
            errorMsg = e.getMessage();
            e.printStackTrace();
            return false;
        }
    }

    @Override
    protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
        HttpServletResponse httpServletResponse = (HttpServletResponse) response;
        httpServletResponse.setStatus(400);
        httpServletResponse.setContentType("application/json;charset=utf-8");
        PrintWriter out = httpServletResponse.getWriter();
        out.println(JSONUtil.toJsonStr(Result.fail(errorMsg)));
        out.flush();
        out.close();
        return false;
    }

    /**
     * 对跨域访问提供支持
     *
     * @param request
     * @param response
     * @return
     * @throws Exception
     */
    @Override
    protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {
        HttpServletRequest httpServletRequest = (HttpServletRequest) request;
        HttpServletResponse httpServletResponse = (HttpServletResponse) response;
        httpServletResponse.setHeader("Access-control-Allow-Origin", httpServletRequest.getHeader("Origin"));
        httpServletResponse.setHeader("Access-Control-Allow-Methods", "GET,POST,OPTIONS,PUT,DELETE");
        httpServletResponse.setHeader("Access-Control-Allow-Headers", httpServletRequest.getHeader("Access-Control-Request-Headers"));
        // 跨域发送一个option请求
        if (httpServletRequest.getMethod().equals(RequestMethod.OPTIONS.name())) {
            httpServletResponse.setStatus(HttpStatus.OK.value());
            return false;
        }
        return super.preHandle(request, response);
    }

}
复制代码

3.JwtToken

shiro 在没有和 jwt 整合之前,用户的账号密码被封装成了 UsernamePasswordToken 对象,UsernamePasswordToken 其实是 AuthenticationToken 的实现类。

这里既然要和 jwt 整合,JWTFilter 传递给 Realm 的 token 必须是 AuthenticationToken 的实现类。

6.配置 Shiro

1.ShiroConfig

ShiroConfig 主要包含 2 部分:过滤器、安全管理器

过滤器:

安全管理器:

2.自定义 Realm

自定义 Realm 的认证方法主要用来校验 token 的合法性:

@Component
public class MyRealm extends AuthorizingRealm {

    @Autowired
    private RedisUtil redisUtil;
    @Autowired
    private JwtUtil jwtUtil;

    /**
     * 限定这个realm只能处理JwtToken
     */
    @Override
    public boolean supports(AuthenticationToken token) {
        return token instanceof JwtToken;
    }

    /**
     * 授权(授权部分这里就省略了)
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        // 获取到用户名,查询用户权限
        return null;
    }

    /**
     * 认证
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken){
        // 获取token信息
        String token = (String) authenticationToken.getCredentials();
        // 校验token:未校验通过或者已过期
        if (!jwtUtil.verifyToken(token) || jwtUtil.isExpire(token)) {
            throw new AuthenticationException("token已失效,请重新登录");
        }
        //用户信息
        User user = (User) redisUtil.get("token_" + token);
        if (null == user) {
            throw new UnknownAccountException("用户不存在");
        }
        SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo(user, token, this.getName());
        return simpleAuthenticationInfo;
    }
}
复制代码

7.测试

1.登录

@PostMapping("/login")
public Result login(@RequestParam String username, @RequestParam String password) {
    // 从数据库中查找用户的信息,信息正确生成token
    return userService.login(username, password);
}
复制代码

2.访问资源

token 失效:

token 正常:

8.完整代码

链接: https://pan.baidu.com/s/1kbGI0nyfRMjgKKYd208f5w?pwd=1234 
提取码: 1234 
复制代码

Acho que você gosta

Origin juejin.im/post/7158077107958972424
Recomendado
Clasificación