primavera-boot-2 + shiro + JWT lograr una gestión integrada de los derechos

Después de años de vacío, pasé algún tiempo investigando Apache Shiro. Muestra Dirección Código

https://github.com/CodingSoldier/java-learn/tree/master/project/shiro/shiro-jwt

Si usted no entiende shiro Apache, ofrezco mi camino de aprendizaje:

1, la primera lección para aprender tutoriales gratuitos red Mu " marco de seguridad Shiro Getting Started " https://www.imooc.com/learn/977 .

2, a continuación, cargar lecciones aprendidas tutoriales red Mu "sistema de desarrollo de la gestión de los derechos de Java empresariales", sólo se enteró Capítulo 3 Apache Shiro teoría marco de derechos y ejercicios prácticos https://coding.imooc.com/class/chapter/149.html #anchor .

3, los documentos shiro última escolares, 12 tienen una versión china del documento, un nivel de traducción no es muy bueno, un poco mejor que la barra de Google Traductor. https://www.ctolib.com/docs/sfile/apache-shiro-reference/

primavera-boot-2 + shiro + JWT integración

dependencia pom.xml

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>

        <!--spring-boot与shiro的整合包-->
        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-spring-boot-web-starter</artifactId>
            <version>1.4.2</version>
        </dependency>

        <!--jwt依赖-->
        <dependency>
            <groupId>com.auth0</groupId>
            <artifactId>java-jwt</artifactId>
            <version>3.2.0</version>
        </dependency>

shiro específico de la materia, los directores:

sujeto es un término de seguridad, se refiere a un usuario particular de un seguro de aplicación "vista". Shiro ejemplo Sujeto representa un estado seguro de un único funcionamiento de la aplicación y el usuario. Para aplicaciones web, en la mayoría de los casos objeto es el usuario, puede ser un cliente.

directores (identidad) es el tema de "propiedad de identidad." Por ejemplo: ID de usuario, nombre de usuario, ID de cliente. En otras entidades de seguridad Resorte del marco de seguridad también se utilizan para identificar de forma única al usuario.

Puede entenderse a partir del código siguiente

    @Test
    public void test1(){

        SimpleAccountRealm sar = new SimpleAccountRealm();
        sar.addAccount("username01", "pwd01");

        DefaultSecurityManager dsm = new DefaultSecurityManager();
        dsm.setRealm(sar);

        SecurityUtils.setSecurityManager(dsm);
        Subject subject = SecurityUtils.getSubject();
        System.out.println("subject未登陆,principals是null: "+subject.getPrincipals());

        UsernamePasswordToken token = new UsernamePasswordToken("username01", "pwd01");

        subject.login(token);
        System.out.println("subject登陆成功后,principals是UsernamePasswordToken中的第一个参数: "+subject.getPrincipals());

    }

¿Por qué después del aterrizaje exitoso tema de la identidad atributos principales son UsernamePasswordToken el nombre de usuario de la misma?

método cubierta de la interfaz fuente UsernamePasswordToken AuthenticationToken getPrincipal () porque

Objeto pública getPrincipal () {
    this.getUsername retorno ();
}

Este método devuelve el primer parámetro UsernamePasswordToken nombre de usuario.

Use default UsernamePasswordToken aterrizaje, Shiro directores y sujeto de estado de certificación se almacena en la sesión, pero la sesión tiene java es menos común, y más comúnmente utilizado es simbólica. Se utiliza en la presente jwt generado token.

Hay varios conceptos que necesita saber:

shiro la autenticación y autorización de usuarios de usuario están separados. La autenticación de usuario (puede ser entendido como el aterrizaje) llama al usuario Autenticación, Autorización llamada La autorización, que se ve un poco como dos palabras. Cuando golpeó con una clase o método que indicaría la autenticación y la autenticación relacionada, la autorización, e indica encontró relacionada autorización.

Realm se almacena en la seguridad de los datos de cliente como un componente de los usuarios, roles, permisos, etc., puede ser entendida como DAO.

shiro primero con las intersecciones de filtro la solicitud y luego llama reino para obtener la autenticación de usuario, información de autorización.

Éstos son algunos aspectos destacados de código

subject.login método parámetro recibido (token) es AuthenticationToken, una nueva herencia JwtToken AuthenticationToken.

public class JwtToken implements AuthenticationToken {
    private String token;
    public JwtToken(String token) {
        this.token = token;
    }

    // token作为principal,
    @Override
    public Object getPrincipal() {
        return this.token;
    }

    // 由于getPrincipal没返回username,而是返回token,所以credentials设置为空字符串即可
    @Override
    public Object getCredentials() {
        return Constant.CREDENTIALS_EMPTY;
    }
}

Nueva JwtFilter herencia BasicHttpAuthenticationFilter, ver la palabra de autenticación, todo el mundo debe conocer el papel de este filtro es la autenticación de usuario.

@Slf4j
public class JwtFilter extends BasicHttpAuthenticationFilter {

    @Override
    protected AuthenticationToken createToken(ServletRequest request, ServletResponse response) {
        // 获取请求头Authorization的值
        String authorization = getAuthzHeader(request);
        return new JwtToken(authorization);
    }

    /**
     * 执行登录操作
     * 大部分代码跟父类一样,不同之处是catch到异常后返回自定义异常给前端
     */
    @Override
    protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception {
        AuthenticationToken token = this.createToken(request, response);
        if (token == null) {
            String msg = "createToken method implementation returned null. A valid non-null AuthenticationToken must be created in order to execute a login attempt.";
            throw new IllegalStateException(msg);
        } else {
            try {
                Subject subject = this.getSubject(request, response);
                subject.login(token);
                return this.onLoginSuccess(token, subject, request, response);
            } catch (AuthenticationException e) {
                Result result = Result.fail("用户认证异常");
                if (e.getCause() instanceof CustomException){
                    CustomException customException = (CustomException)e.getCause();
                    result = Result.fail(customException.getCode(), customException.getMessage());
                }
                WebUtil.sendResponse(response, result);
                return false;
            }
        }
    }

    /**
     * 身份认证未通过,执行此方法
     * 返回true,继续处理请求
     * 返回false,不继续处理请求,结束过滤器链
     * BasicHttpAuthenticationFilter源码中也是在onAccessDenied()方法内调用executeLogin
     */
    @Override
    protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
        boolean r;
        String authorization = getAuthzHeader(request);
        if (StringUtils.isEmpty(authorization)){
            WebUtil.sendResponse(response, Result.fail(Constant.CODE_TOKEN_ERROR,"无token"));
            r = false;
        }else {
            r = executeLogin(request, response);
        }
        return r;
    }
    
}

shiro filtro de permisos por defecto es PermissionsAuthorizationFilter, cuando el usuario no tiene acceso a la interfaz, la información se devuelve a la parte delantera de la antipática, escribimos una CustomPermissionsAuthorizationFilter heredó PermissionsAuthorizationFilter devolver un mensaje personalizado en la parte delantera. Autorización ver esta palabra, sabe que este filtro se asocia con el usuario autorizado.

@Slf4j
public class CustomPermissionsAuthorizationFilter extends PermissionsAuthorizationFilter {

    /**
     * 用户无权访问url时,此方法会被调用
     * 默认实现为org.apache.shiro.web.filter.authz.AuthorizationFilter#onAccessDenied()
     * 覆盖父类的方法,返回自定义信息给前端
     *
     * 接口doc上说:
     *    AuthorizationFilter子类(权限授权过滤器)的onAccessDenied()应该永远返回false,那么在onAccessDenied()内就必然要发送response响应给前端,不然前端就收不到任何数据
     *    AuthenticationFilter、AuthenticatingFilter子类(身份认证过滤器)的onAccessDenied()的返回值则没有限制
     */
    @Override
    protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws IOException {
        WebUtil.sendResponse(response, Result.fail("权限不足"));
        return false;
    }

}

Para construir un JwtRealm

public class JwtRealm extends AuthorizingRealm {

    private UserService userService;

    /**
     * 最好保持单例
     * JwtRealm可以不交给spring管理,在创建JwtRealm的时候需要创建者传递参数UserService
     */
    public JwtRealm(UserService userService) {
        this.userService = userService;

        /**
         * 启动认证缓存,默认是false。源码如下
         * org.apache.shiro.realm.AuthenticatingRealm#AuthenticatingRealm()
         *     this.authenticationCachingEnabled = false;
         */
        this.setAuthenticationCachingEnabled(true);

        /**
         * 启动授权缓存,默认就是true,代码如下
         * org.apache.shiro.realm.AuthorizingRealm#AuthorizingRealm()
         *     this.authorizationCachingEnabled = true;
         */
        // this.setAuthorizationCachingEnabled(true);

        // 设置缓存管理器,使用shiro自带的MemoryConstrainedCacheManager即可
        this.setCacheManager(new MemoryConstrainedCacheManager());

    }

    // subject.login(token)方法中的token是JwtToken时,调用此Realm的doGetAuthenticationInfo
    @Override
    public boolean supports(AuthenticationToken token) {
        return token instanceof JwtToken;
    }

    /**
     * 认证用户
     * 本方法被 org.apache.shiro.realm.AuthenticatingRealm#getAuthenticationInfo()调用
     *    AuthenticationInfo info = this.getCachedAuthenticationInfo(token);
     *    如果通过token在缓存中获取到用户认证,就不调用本方法
     *
     * 补充一点:用户认证接口往往只传递principals信息,不传credentials。在spring security中也是这种思路
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws CustomAuthenticationException {
        String token = (String) authenticationToken.getPrincipal();
        if (StringUtils.isEmpty(token)){
            throw new CustomAuthenticationException("token为空");
        }
        String username = JWTUtil.getUsername(token);
        User user = userService.getUser(username);
        if (user == null){
            throw new CustomAuthenticationException("无此用户");
        }
        /**
         * token无效,抛出异常
         * MyControllerAdvice捕获MyException异常后,将Constant.CODE_TOKEN_ERROR返回给前端,前端收到此code后跳转登录页
         */
        if (!JWTUtil.verify(token, username, user.getPassword())){
            throw new CustomAuthenticationException(Constant.CODE_TOKEN_ERROR, "token无效,请重新登录");
        }
        if (JWTUtil.isExpired(token)){
            throw new CustomAuthenticationException(Constant.CODE_TOKEN_ERROR, "token无效,请重新登录");
        }
        return new SimpleAuthenticationInfo(token, Constant.CREDENTIALS_EMPTY, this.getName());
    }

    /**
     * 用户授权
     * 本方法被 org.apache.shiro.realm.AuthorizingRealm#getAuthorizationInfo() 调用
     *     Cache<Object, AuthorizationInfo> cache = this.getAvailableAuthorizationCache();
     *     如果获取到缓存,就通过cache.get(key)获取授权数据,key就是principal
     *
     *     若无缓存,在调用了本方法后,会将本方法返回的AuthorizationInfo添加到缓存中
     *     cache.put(key, info);
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        String username = JWTUtil.getUsername(principalCollection.getPrimaryPrincipal().toString());
        User user = userService.getUser(username);

        SimpleAuthorizationInfo sai = new SimpleAuthorizationInfo();

        List<String> roleList = new ArrayList<>();
        List<String> permissionList = new ArrayList<>();
        if (!CollectionUtils.isEmpty(user.getRoleList())){
            for (Role role:user.getRoleList()){
                roleList.add(role.getName());
                if (!CollectionUtils.isEmpty(role.getPermissionList())){
                    for (Permission permission:role.getPermissionList()){
                        permissionList.add(permission.getName());
                    }
                }
            }
        }
        sai.addRoles(roleList);
        sai.addStringPermissions(permissionList);

        return sai;
    }
}

clase de configuración shiro

@Configuration
public class ShiroConfig {

    // 使用@Lazy避免UserService为空
    @Lazy
    @Autowired
    UserService userService;

    // 创建jwtRealm
    @Bean
    public JwtRealm jwtRealm(){
        return new JwtRealm(userService);
    }

    @Bean
    public DefaultWebSecurityManager securityManager(@Qualifier("jwtRealm") JwtRealm jwtRealm){
        DefaultWebSecurityManager manager = new DefaultWebSecurityManager();
        // 设置realm
        manager.setRealm(jwtRealm);

        /**
         * 禁止session持久化存储
         * 一定要禁止session持久化。不然清除认证缓存、授权缓存后,shiro依旧能从session中读取到认证信息
         */
        DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
        DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator();
        defaultSessionStorageEvaluator.setSessionStorageEnabled(false);
        subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator);
        manager.setSubjectDAO(subjectDAO);

        return manager;
    }

    @Bean
    public ShiroFilterFactoryBean shiroFilterFactoryBean(@Qualifier("securityManager") DefaultWebSecurityManager securityManager){
        ShiroFilterFactoryBean factoryBean = new ShiroFilterFactoryBean();

        factoryBean.setSecurityManager(securityManager);

        //添加filter,factoryBean.getFilters()获取到的是引用,可直接添加值
        factoryBean.getFilters().put("jwt", new JwtFilter());
        factoryBean.getFilters().put("customPerms", new CustomPermissionsAuthorizationFilter());

        /**
         * factoryBean.getFilterChainDefinitionMap();默认是size=0的LinkedHashMap
         */
        Map<String, String> definitionMap = factoryBean.getFilterChainDefinitionMap();

        /**
         * definitionMap是一个LinkedHashMap,是一个链表结构
         * put的顺序很重要,当/open/**匹配到请求url后,将不再检查后面的规则
         * 官方将这种原则称为 FIRST MATCH WINS
         * https://waylau.gitbooks.io/apache-shiro-1-2-x-reference/content/III.%20Web%20Applications/10.%20Web.html
         */
        definitionMap.put("/open/**", "anon");

        /**
         * 由于禁用了session存储,shiro不会存储用户的认证状态,所以在接口授权之前要先认证用户,不然CustomPermissionsAuthorizationFilter不知道用户是谁
         * 实际项目中可以将这些接口权限规则放到数据库中去
         */
        definitionMap.put("/user/add", "jwt, customPerms["+userService.getUserAdd().getName()+"]");
        definitionMap.put("/user/delete", "jwt, customPerms["+userService.getUserDelete().getName()+"]");
        definitionMap.put("/user/edit", "jwt, customPerms["+userService.getUserEdit().getName()+"]");
        definitionMap.put("/user/view", "jwt, customPerms["+userService.getUserView().getName()+"]");
        definitionMap.put("/test/other", "jwt, customPerms["+userService.getTestOther().getName()+"]");
        definitionMap.put("/role/permission/edit", "jwt, customPerms["+userService.getRolePermisssionEdit().getName()+"]");

        // 前面的规则都没匹配到,最后添加一条规则,所有的接口都要经过com.example.shirojwt.filter.JwtFilter这个过滤器验证
        definitionMap.put("/**", "jwt");

        factoryBean.setFilterChainDefinitionMap(definitionMap);

        return factoryBean;
    }
}

En primer lugar pedir a alguien a mi github descarga el código, que se ejecuta en sus ordenadores.

shiro autenticación, autorización procesa en este proyecto

Crear un aterrizaje de interfaz

@RestController
@Slf4j
@RequestMapping("/open")
public class OpenApiCtrl {
    @Autowired
    UserService userService;

    /**
     * 登陆
     * 开放接口,不使用shiro拦截,生成令牌并返回给前端
     * 用户名             密码
     * admin           admin-pwd
     * cudrOtherUser   cudrOtherUser-pwd
     * viewUser        viewUser-pwd
     */
    @PostMapping("/login")
    public Result openLogin(@RequestBody User userVo){
        String username = userVo.getUsername();
        String password = userVo.getPassword();

        // 用户名密码校验
        User user = userService.getUser(username);
        if (user == null){
            throw new CustomException("无此用户");
        }
        if (!user.getPassword().equals(new Md5Hash(password).toString())){
            throw new CustomException("用户名或密码错误");
        }

        // 生成令牌
        String token = JWTUtil.sign(username, user.getPassword());

        /**
         * 在登陆接口中就执行shiro用户认证,用于测试不禁用session存储的情形
         */
        //JwtToken jwtToken = new JwtToken(token);
        //Subject subject = SecurityUtils.getSubject();
        //subject.login(jwtToken);

        return Result.success(token);
    }

}

La parte delantera de la ficha para obtener modo, a continuación, las solicitudes se ponen en la cabecera de Autorización = símbolo

A continuación, enviar una solicitud

rizo --location --request GET 'localhost: 8080 / user / add' \
--header 'Autorización: XXXXX'

Esta solicitud a través de los dos filtros, JwtFilter y CustomPermissionsAuthorizationFilter, realiza la autenticación y autorización de usuarios.

proceso de certificación es la siguiente:

. 1, AccessControlFilter # onPreHandle ()
2, AuthenticationFilter isAccessAllowed # (), este método sólo determina si el sujeto actual (usuario sujeto) ha sido autenticada, el resultado subject.authenticated no autenticado.
3, JwtFilter # onAccessDenied (), el usuario no está autenticado, se llama a este método.
4, JwtFilter # executeLogin (), utilizando la generación de Autorización de encabezado de solicitud JwtToken, a continuación, realizar subject.login (JwtToken);
. 5, # AuthenticatingRealm getAuthenticationInfo (), por JwtToken caché de consultas AuthenticationInfo
. 6, no caché AuthenticationInfo, introduzca JwtRealm # doGetAuthenticationInfo (), el cheque simbólico legitimidad y generar SimpleAuthenticationInfo.

7, DefaultWebSubjectFactory # createSubject, el uso SimpleAuthenticationInfo los directores generar un sujeto verificado, autenticación de usuarios se ha completado.

proceso de autorización es la siguiente:

. 1, AccessControlFilter # onPreHandle ()
2, PermissionsAuthorizationFilter isAccessAllowed # (), la determinación de si el sujeto incluye un permiso interfaz.
3, AuthorizingRealm # getAuthorizationInfo (), para obtener la autorización de caché.
4, sin caché de autorización para obtener información de autorización JwtRealm # doGetAuthorizationInfo ().

caso de prueba de la utilización de la sesión. pasos de preparación son los siguientes:

1, el OpenApiCtrl # openLogin () en la liberación de código anotada.

JwtToken jwtToken = nuevo JwtToken (token);
Materias = SecurityUtils.getSubject ();
subject.login (jwtToken);

2, ShiroConfig # SecurityManager () comentó deshabilitar código de sesión almacenada.

//DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
//DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator();
//defaultSessionStorageEvaluator.setSessionStorageEnabled(false);
//subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator);
//manager.setSubjectDAO(subjectDAO);

3, ShiroConfig # shiroFilterFactoryBean (), el permiso de usar la interfaz sólo CustomPermissionsAuthorizationFilter

definitionMap.put ( "/ user / add", "customPerms [" + userService.getUserAdd () getName () +. "]");
definitionMap.put ( "/ usuario / borrar", "customPerms [". + userService.getUserDelete () getName () + "]");
definitionMap.put ( "/ usuario / editar", "customPerms [". + userService.getUserEdit () getName () + "]");
definitionMap.put ( "/ usuario / vista", "customPerms [" + userService.getUserView () getName () +. "]");
definitionMap.put ( "/ prueba / otros", "customPerms [". + userService.getTestOther () getName () + "]");
definitionMap.put ( "/ papel / autorización / editar", "customPerms [" + userService.getRolePermisssionEdit () getName () +. "]");

Inicio del Proyecto

1, utilizando la primera localhost solicitud cartero: 8080 / / login abierto

2, a continuación, utilizar la solicitud cartero localhost: 8080 / usuario / vista, pero la cabecera sin autorización, se encuentra el pedido todavía tenga éxito.

¿Por qué es esto?

1, encontrar PermissionsAuthorizationFilter # isAccessAllowed (), y hay una línea de código 

Materias = getSubject (solicitud, respuesta);

Hablamos de, el sujeto está de usuario shiro, tema de los directores como identificador de usuario, ¿cómo se sabe que el usuario shiro que inició la solicitud es?

2, DefaultWebSubjectFactory # createSubject () Este método tiene el código siguiente

Directores PrincipalCollection = wsc.resolvePrincipals ();
boolean autenticado = wsc.resolveAuthenticated ();

Nos depurar el código fuente de saber, sin desactivar la sesión, shiro los directores (identidad del usuario autenticado), (estado de autenticación) almacenados en la sesión en el.

Esta es la razón por la solicitud localhost: 8080 / usuario / ver independientemente de la hora con o sin razón Autorización encabezado de solicitud puede ser autorizada.

Si las cookies, la sesión se eliminan cartero

Solicitud de nuevo localhost: 8080 / usuario / vista. fuente de depuración
directores PrincipalCollection = wsc.resolvePrincipals (); // directores es nula
Boolean wsc.resolveAuthenticated autenticados = (); // autenticados es falsa

CustomPermissionsAuthorizationFilter # onAccessDenied () se llama, el retorno a la información "privilegios insuficientes".

Borrar caché regular

    /**
     * 测试清除jwtRealm缓存
     * MemoryConstrainedCacheManager使用的是Map作为缓存,必须用定时器清理
     */
    @GetMapping("/cache/test")
    public Result test2(){
        Cache<Object, AuthenticationInfo> authen = jwtRealm.getAuthenticationCache();
        Cache<Object, AuthorizationInfo> author = jwtRealm.getAuthorizationCache();
        log.info("当前缓存, 认证缓存 = {} 授权缓存 = {}", authen, author);

        authen.clear();
        author.clear();
        log.info("缓存清除完成,认证缓存size = {} 授权缓存size = {}", authen.size(), author.size());

        return Result.success("test");
    }

Permisos de configuración Modificar

    /**
     * 测试动态修改接口授权配置
     */
    @GetMapping("/definition/test")
    public Result test1() throws Exception{

        AbstractShiroFilter shiroFilter = (AbstractShiroFilter)shiroFilterFactoryBean.getObject();
        PathMatchingFilterChainResolver filterChainResolver = (PathMatchingFilterChainResolver) shiroFilter.getFilterChainResolver();
        DefaultFilterChainManager manager = (DefaultFilterChainManager) filterChainResolver.getFilterChainManager();
        // 清空老的权限控制
        manager.getFilterChains().clear();
        shiroFilterFactoryBean.getFilterChainDefinitionMap().clear();

        // 生成新的definitionMap
        LinkedHashMap<String, String> definitionMap = new LinkedHashMap<>();
        definitionMap.put("/open/**", "anon");

        definitionMap.put("/user/delete", "jwt, customPerms["+userService.getUserDelete().getName()+"]");
        definitionMap.put("/user/edit", "jwt, customPerms["+userService.getUserEdit().getName()+"]");
        definitionMap.put("/user/add", "jwt, customPerms["+userService.getUserAdd().getName()+"]");
        definitionMap.put("/user/view", "jwt, customPerms["+userService.getUserView().getName()+"]");
        definitionMap.put("/test/other", "jwt, customPerms["+userService.getTestOther().getName()+"]");
        definitionMap.put("/role/permission/edit", "jwt, customPerms["+userService.getRolePermisssionEdit().getName()+"]");

        definitionMap.put("/**", "jwt");

        shiroFilterFactoryBean.setFilterChainDefinitionMap(definitionMap);

        // 重新构建生成权限过滤链
        for (Map.Entry<String, String> entry : definitionMap.entrySet()) {
            String url = entry.getKey();
            String chainDefinition = entry.getValue();
            manager.createChain(url, chainDefinition);
        }

        return Result.success("test");
    }

 

Publicado 51 artículos originales · elogios ganado 14 · Vistas a 40000 +

Supongo que te gusta

Origin blog.csdn.net/u010606397/article/details/104110093
Recomendado
Clasificación