Shiro y JWT implementan el inicio de sesión de usuario y la administración de autoridad

Antes de comenzar, comprendamos dos conceptos

Autenticación

Lo principal que debe hacer en el proceso de autenticación es averiguar quién es el visitante, ¿se ha registrado en nuestro sistema? ¿Es un usuario en nuestro sistema? En caso afirmativo, ¿hemos incluido en la lista negra a este usuario? Esto es lo que hace la certificación. Por ejemplo: cuando ingresa a un área turística, primero debe comprar un boleto. Si no tiene un boleto en la puerta de entrada, el tío / tía lo bloqueará directamente. ¿Compró el boleto hoy? Ha caducado ... Si tiene el boleto y todo está normal, puede ingresar. Este es un proceso de certificación!

Autorización

¡El papel de la autorización es verificar que tiene derecho a acceder a un determinado recurso! Todos sabemos que en la base de datos MySQL, el usuario root puede agregar, eliminar, modificar y verificar los datos de cada biblioteca, pero los usuarios comunes solo pueden tener el derecho de ver, pero no el derecho de borrar y modificar. Lo mismo es cierto en el programa, hay un requisito muy común: el usuario A puede acceder y modificar una página, ya que el usuario B solo puede tener derecho a acceder a esta página, ¡pero no se puede modificar!


Una breve introducción a shiro y jwt

jwt: JSON Web Token, que consta de tres partes: encabezado de solicitud, cuerpo de solicitud y firma. Se utiliza principalmente para la autenticación.
Shiro es un marco de seguridad ligero que puede usar anotaciones para implementar rápidamente la administración de permisos. Se utiliza principalmente para la autorización.


La idea general de lograr autenticación y autorización

El cliente accede al servidor y el servidor autentica la solicitud, incluso si el nombre de usuario y la contraseña son correctos. Si la autenticación es exitosa, se emitirá un token de credencial para el cliente, y el cliente llevará este token cuando acceda al servidor más tarde, de lo contrario O si se manipula el token, ¡la autenticación fallará! Si la autenticación del token es exitosa y el cliente accede al recurso, ¡el servidor también verificará si el usuario tiene derecho a acceder a este recurso! Si este usuario no tiene derecho a acceder a este recurso, ¡el acceso también fallará!

Diseño de tabla de base de datos

权限的认证和校验采用RBAC模型,总共有5张表,分别为user、user_role、role、role_permission和permission,
其中user_role和role_permission为中间表,user表和role表示一对多的关系,
role表和permission表也是一对多的关系。


Implementación de código

El primero es el código de inicio de sesión, que es el código utilizado para la autenticación. Cuando el usuario se autentica correctamente, se devolverá un token al cliente. Si la autenticación falla, ¡el cliente no podrá obtener el token!

Iniciar sesión

@RestController
@Slf4j
public class LoginController {
    @Resource
    private UserService userService;

    @PostMapping("/login")
    public ResponseBean login(@RequestBody JSONObject requestJson) {
        log.info("用户登录");
        String username = requestJson.getString("username");
        String password = requestJson.getString("password");

        if (StringUtils.isBlank(username) || StringUtils.isBlank(password)) {
            throw new RuntimeException("用户名和密码不可以为空!");
        }

        // 从数据库中根据用户名查找该用户信息
        UserDTO userDTO = userService.getByUseName(username);

        // 获取盐
        String salt = userDTO.getSalt();
        // 原密码加密(通过username + salt作为盐)
        String encodedPassword = ShiroKit.md5(password, username + salt);
        if (null != userDTO && userDTO.getPassword().equals(encodedPassword)) {
            return new ResponseBean(200, "Login success", JWTUtil.sign(username, encodedPassword));
        } else {
            throw new UnauthorizedException("用户名或者密码错误!");
        }
    }

}

JWT genera tokens (algunas clases de herramientas con verificación)

public class JWTUtil {
    final static Logger logger = LogManager.getLogger(JWTUtil.class);

    /**
     * 过期时间30分钟
     */
    private static final long EXPIRE_TIME = 30 * 60 * 1000;

    /**
     * 校验token是否正确
     *
     * @param token  密钥
     * @param secret 用户的密码
     * @return 是否正确
     */
    public static boolean verify(String token, String username, String secret) {
        try {
            Algorithm algorithm = Algorithm.HMAC256(secret);
            JWTVerifier verifier = JWT.require(algorithm)
                    .withClaim("username", username)
                    .build();
            verifier.verify(token);
            return true;
        } catch (Exception exception) {
            return false;
        }
    }

    /**
     * 获得token中的信息无需secret解密也能获得
     *
     * @return token中包含的用户名
     */
    public static String getUsername(String token) {
        try {
            DecodedJWT jwt = JWT.decode(token);
            return jwt.getClaim("username").asString();
        } catch (JWTDecodeException e) {
            return null;
        }
    }

    /**
     * 生成签名,30min后过期
     *
     * @param username 用户名
     * @param secret   用户的密码
     * @return 加密的token
     */
    public static String sign(String username, String secret) {
        try {
            Date date = new Date(System.currentTimeMillis() + EXPIRE_TIME);
            //使用用户自己的密码充当加密密钥
            Algorithm algorithm = Algorithm.HMAC256(secret);
            // 附带username信息
            //  建造者模式
            String jwtString = JWT.create()
                    .withClaim("username", username)
                    .withExpiresAt(date)
                    .sign(algorithm);
            logger.debug(String.format("JWT:%s", jwtString));
            return jwtString;
        } catch (UnsupportedEncodingException e) {
            return null;
        }
    }
}

Filtro

Excluya la solicitud de inicio de sesión, y luego todas las demás solicitudes deben pasar por el proceso de autenticación antes de la solicitud:

public class JWTFilter extends BasicHttpAuthenticationFilter {

    private Logger LOGGER = LoggerFactory.getLogger(this.getClass());

    /**
     * 判断用户是否想要登入。
     * true:是要登录
     * 检测header里面是否包含Authorization字段即可
     */
    @Override
    protected boolean isLoginAttempt(ServletRequest request, ServletResponse response) {
        HttpServletRequest req = (HttpServletRequest) request;
        String authorization = req.getHeader("Authorization");
        return authorization != null;
    }

    /**
     *
     */
    @Override
    protected boolean executeLogin(ServletRequest request, ServletResponse response)  throws Exception {
        HttpServletRequest httpServletRequest = (HttpServletRequest) request;
        String authorization = httpServletRequest.getHeader("Authorization");

        JWTToken token = new JWTToken(authorization);
        // 提交给realm进行登入,如果错误他会抛出异常并被捕获
        getSubject(request, response).login(token);
        // 如果没有抛出异常则代表登入成功,返回true
        return true;
    }

    /**
     * 这里我们详细说明下为什么最终返回的都是true,即允许访问
     * 例如我们提供一个地址 GET /article
     * 登入用户和游客看到的内容是不同的
     * 如果在这里返回了false,请求会被直接拦截,用户看不到任何东西
     * 所以我们在这里返回true,Controller中可以通过 subject.isAuthenticated() 来判断用户是否登入
     * 如果有些资源只有登入用户才能访问,我们只需要在方法上面加上 @RequiresAuthentication 注解即可
     * 但是这样做有一个缺点,就是不能够对GET,POST等请求进行分别过滤鉴权(因为我们重写了官方的方法),但实际上对应用影响不大
     */
    @Override
    protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
        if (isLoginAttempt(request, response)) {
            try {
                executeLogin(request, response);
            } catch (Exception e) {
                response401(request, response);
            }
        }
        return true;
    }

    /**
     * 对跨域提供支持
     */
    @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请求,这里我们给option请求直接返回正常状态
        if (httpServletRequest.getMethod().equals(RequestMethod.OPTIONS.name())) {
            httpServletResponse.setStatus(HttpStatus.OK.value());
            return false;
        }
        return super.preHandle(request, response);
    }

    /**
     * 将非法请求跳转到 /401
     */
    private void response401(ServletRequest req, ServletResponse resp) {
        try {
            HttpServletResponse httpServletResponse = (HttpServletResponse) resp;
            httpServletResponse.sendRedirect("/401");
        } catch (IOException e) {
            LOGGER.error(e.getMessage());
        }
    }
}

Personaliza MyRealm

Permisos personalizados y lógica de autenticación de identidad:

@Configuration
public class MyRealm extends AuthorizingRealm {
    @Resource
    private UserService userService;


    /**
     * 必须重写此方法,不然Shiro会报错
     */
    @Override
    public boolean supports(AuthenticationToken token) {
        return token instanceof JWTToken;
    }

    /**
     * 只有当需要检测用户权限的时候才会调用此方法,例如checkRole,checkPermission之类的
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        String username = JWTUtil.getUsername(principals.toString());
        // 根据用户名获取用户角色 权限信息
        UserDTO user = userService.getByUseName(username);
        SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();

        // 获取角色
        List<String> roleNames = getRoleNameList(user.getRoleList());
        for (String roleName : roleNames) {
            simpleAuthorizationInfo.addRole(roleName);
        }

        // 获取权限
        List<String> permissions = getPermissionList(user.getPermissionList());
        simpleAuthorizationInfo.addStringPermissions(new HashSet<>(permissions));
        return simpleAuthorizationInfo;
    }

    /**
     * 获取权限
     *
     * @param permissionList
     * @return
     */
    private List<String> getPermissionList(List<Permission> permissionList) {
        List<String> permissions = new ArrayList<>(permissionList.size());
        for (Permission permission : permissionList) {
            if (StringUtils.isNotBlank(permission.getPerCode())) {
                permissions.add(permission.getPerCode());
            }
        }
        return permissions;
    }

    /**
     * 获取角色名称
     *
     * @param roleList
     * @return
     */
    private List<String> getRoleNameList(List<Role> roleList) {
        List<String> roleNames = new ArrayList<>(roleList.size());
        for (Role role : roleList) {
            roleNames.add(role.getName());
        }
        return roleNames;
    }

    /**
     * 默认使用此方法进行用户名正确与否验证,错误抛出异常即可。
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken auth) throws AuthenticationException {
        String token = (String) auth.getCredentials();
        // 解密获得username,用于和数据库进行对比
        String username = JWTUtil.getUsername(token);
        if (username == null) {
            throw new AuthenticationException("token invalid");
        }

        UserDTO userBean = userService.getByUseName(username);
        if (userBean == null) {
            throw new AuthenticationException("User didn't existed!");
        }

        if (!JWTUtil.verify(token, username, userBean.getPassword())) {
            throw new AuthenticationException("Username or password error");
        }

        return new SimpleAuthenticationInfo(token, token, "my_realm");
    }
}

MyRealm

Custom Realm es la clave para lograr los permisos. MyRealm hereda la clase abstracta AuthorizingRealm del marco de Shiro y luego reescribe el método doGetAuthorizationInfo . En este método, primero tomamos el nombre de usuario del token que lleva el cliente y luego lo verificamos en la base de datos en función del nombre de usuario Enumere los roles y permisos que posee el usuario y luego coloque los roles y permisos en el objeto SimpleAuthorizationInfo respectivamente. Como sigue:

 // 获取角色
        List<String> roleNames = getRoleNameList(user.getRoleList());
        for (String roleName : roleNames) {
            simpleAuthorizationInfo.addRole(roleName);
        }

        // 获取权限
        List<String> permissions = getPermissionList(user.getPermissionList());
        simpleAuthorizationInfo.addStringPermissions(new HashSet<>(permissions));

Comentario de Shiro

1.Requiere autenticación:

  • Las clases, instancias y métodos marcados con esta anotación deben autenticarse en la sesión actual cuando se accede o se llama.

2.RequiresGuest:

  • La clase, instancia y método marcado con esta anotación al acceder o invocar, el Asunto actual puede ser una identidad "racha", no hay necesidad de autenticarse o hay un registro en la sesión original.

3.RequiresPermissions:

  • El Asunto actual debe tener ciertos permisos específicos para ejecutar el método marcado por la anotación. Si el Asunto actual no tiene dicha autoridad, el método no se ejecutará.

4.Requires Roles:

  • El Asunto actual debe tener todos los roles especificados para acceder a los métodos marcados por la anotación. Si el sujeto no tiene todos los roles especificados al mismo tiempo, el método no se ejecutará y se lanzará una AuthorizationException.

5.RequiresUser :

  • El sujeto actual debe ser el usuario de la aplicación para acceder o llamar a las clases, instancias y métodos marcados por la anotación.

A menudo utilizamos Requiere Roles y Requiere Permisos.
Estas anotaciones se usan en orden:

RequiresRoles-->RequiresPermissions-->RequiresAuthentication-->RequiresUser-->RequiresGuest
如果有个多个注解的话,前面的通过了会继续检查后面的,若不通过则直接返回

@RequiresRoles(value = {"admin","guest"},logical = Logical.OR)
   这个注解的作用是携带的token中的角色必须是admin、guest中的一个
   如果把logical = Logical.OR改成logical = Logical.AND,那么这个注解的作用就变成角色必须同时包含admin和guest了.
   
@RequiresPermissions(logical = Logical.OR, value = {"user:view", "user:edit"})
    和@RequiresRoles类似,权限的的值放在value中,值之间的关系用Logical.OR和Logical.AND控制

    

Prueba de código

Iniciar sesión

Inicie sesión para obtener el token

Acceda a los recursos sin llevar un token

Acceda a los recursos sin llevar un token

Acceso con token

Acceso exitoso con token

Requiere rol de invitado pero es un token de administrador

Requiere rol de invitado pero es un token de administrador

token de invitado de permiso de invitado

token de invitado de permiso de invitado

Permisos insuficientes

Permisos insuficientes

Tener acceso

Tener acceso

Código completo del proyecto

Código completo del proyecto GitHub


Publicación de blog de referencia

Publicado 4 artículos originales · elogiado 4 · visitas 103

Supongo que te gusta

Origin blog.csdn.net/qq_40585384/article/details/105345435
Recomendado
Clasificación