Maison  >  Article  >  Java  >  Comment Springboot implémente l'authentification et la gestion dynamique des autorisations

Comment Springboot implémente l'authentification et la gestion dynamique des autorisations

WBOY
WBOYavant
2023-06-03 21:46:061821parcourir

Points de connaissances supplémentaires

Cache Shiro

Analyse des processus

Dans le projet d'origine, puisqu'il n'y a pas de cache configuré, la base de données sera interrogée à chaque fois qu'il sera nécessaire de vérifier si le sujet actuel a des droits d'accès. Étant donné que les données d'autorisation sont généralement des données avec plus de lecture et moins d'écriture, nous devrions y ajouter la prise en charge de la mise en cache.

Lorsque nous ajoutons le cache, Shiro interrogera d'abord le cache pour les données pertinentes lors de l'authentification. S'il n'est pas dans le cache, il interrogera la base de données et écrira les données trouvées dans le cache. cache la prochaine fois qu'il sera vérifié, au lieu de l'obtenir de la base de données. Cela améliorera les performances de notre application.

Ensuite, implémentons la partie gestion du cache de shiro.

Mécanisme de session Shiro

Shiro fournit des fonctions complètes de gestion de session au niveau de l'entreprise, qui ne dépendent pas du conteneur sous-jacent (tel que le conteneur Web Tomcat). Il peut être utilisé dans les environnements JavaSE et JavaEE. surveillance des événements et stockage de session, clustering indépendant du conteneur, prise en charge de l'invalidation/expiration, prise en charge transparente du Web, prise en charge de l'authentification unique SSO et autres fonctionnalités.

Nous utiliserons la gestion de session de Shiro pour reprendre la session Web de notre application et stocker les informations de session via Redis.

Étapes d'intégration

Ajouter du cache

CacheManager

Dans Shiro, il fournit la classe CacheManager pour la gestion du cache.

Utilisez l'implémentation EhCache par défaut de Shiro

Dans Shiro, le cadre de mise en cache EhCache est utilisé par défaut. EhCache est un framework de mise en cache in-process purement Java, rapide et simple.

Présentation de la dépendance shiro-EhCache
<dependency>
    <groupId>org.apache.shiro</groupId>
    <artifactId>shiro-ehcache</artifactId>
    <version>1.4.0</version>
</dependency>

Dans le processus d'intégration de Redis avec SpringBoot, vous devez également faire attention au problème de correspondance de version, sinon une exception de méthode non trouvée peut être signalée.

Ajoutez la configuration du cache dans ShiroConfig
private void enableCache(MySQLRealm realm){
    //开启全局缓存配置
    realm.setCachingEnabled(true);
    //开启认证缓存配置
    realm.setAuthenticationCachingEnabled(true);
    //开启授权缓存配置
    realm.setAuthorizationCachingEnabled(true);

    //为了方便操作,我们给缓存起个名字
    realm.setAuthenticationCacheName("authcCache");
    realm.setAuthorizationCacheName("authzCache");
    //注入缓存实现
    realm.setCacheManager(new EhCacheManager());
}

Ensuite, appelez cette méthode dans getRealm.

Conseils : Dans cette implémentation, seule la mise en cache locale est implémentée. En d’autres termes, les données mises en cache partagent la même mémoire machine que l’application. Si le serveur tombe en panne ou subit une panne de courant inattendue, les données mises en cache n'existeront plus. Bien sûr, vous pouvez également donner au cache plus de configurations via la méthode cacheManager.setCacheManagerConfigFile().

Ensuite, nous mettrons en cache nos données d'autorisation via Redis

Utilisez Redis pour implémenter

Ajouter des dépendances
<!--shiro-redis相关依赖-->
        <dependency>
            <groupId>org.crazycake</groupId>
            <artifactId>shiro-redis</artifactId>
            <version>3.1.0</version>
            <!--    里面这个shiro-core版本较低,会引发一个异常
					ClassNotFoundException: org.apache.shiro.event.EventBus
                    需要排除,直接使用上面的shiro
                    shiro1.3 加入了时间总线。-->
            <exclusions>
                <exclusion>
                    <groupId>org.apache.shiro</groupId>
                    <artifactId>shiro-core</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
Configurer redis

Ajouter la configuration liée à Redis dans application.yml

spring:
   redis:
     host: 127.0.0.1
     port: 6379
     password: hewenping
     timeout: 3000
     jedis:
       pool:
         min-idle: 5
         max-active: 20
         max-idle: 15

Modifier la classe de configuration ShiroConfig et ajouter le plug shiro-redis -dans la configuration

/**shiro配置类
 * @author 赖柄沣 bingfengdev@aliyun.com
 * @version 1.0
 * @date 2020/10/6 9:11
 */
@Configuration
public class ShiroConfig {

    private static final String CACHE_KEY = "shiro:cache:";
    private static final String SESSION_KEY = "shiro:session:";
    private static final int EXPIRE = 18000;
    @Value("${spring.redis.host}")
    private String host;
    @Value("${spring.redis.port}")
    private int port;
    @Value("${spring.redis.timeout}")
    private int timeout;
    @Value("${spring.redis.password}")
    private String password;

    @Value("${spring.redis.jedis.pool.min-idle}")
    private int minIdle;
    @Value("${spring.redis.jedis.pool.max-idle}")
    private int maxIdle;
    @Value("${spring.redis.jedis.pool.max-active}")
    private int maxActive;

    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(org.apache.shiro.mgt.SecurityManager securityManager) {
        AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
        authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
        return authorizationAttributeSourceAdvisor;
    }


    /**
     * 创建ShiroFilter拦截器
     * @return ShiroFilterFactoryBean
     */
    @Bean(name = "shiroFilterFactoryBean")
    public ShiroFilterFactoryBean getShiroFilterFactoryBean(DefaultWebSecurityManager securityManager){
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        shiroFilterFactoryBean.setSecurityManager(securityManager);

        //配置不拦截路径和拦截路径,顺序不能反
        HashMap<String, String> map = new HashMap<>(5);

        map.put("/authc/**","anon");
        map.put("/login.html","anon");
        map.put("/js/**","anon");
        map.put("/css/**","anon");

        map.put("/**","authc");
        shiroFilterFactoryBean.setFilterChainDefinitionMap(map);

        //覆盖默认的登录url
        shiroFilterFactoryBean.setLoginUrl("/authc/unauthc");
        return shiroFilterFactoryBean;
    }

    @Bean
    public Realm getRealm(){
        //设置凭证匹配器,修改为hash凭证匹配器
        HashedCredentialsMatcher myCredentialsMatcher = new HashedCredentialsMatcher();
        //设置算法
        myCredentialsMatcher.setHashAlgorithmName("md5");
        //散列次数
        myCredentialsMatcher.setHashIterations(1024);
        MySQLRealm realm = new MySQLRealm();
        realm.setCredentialsMatcher(myCredentialsMatcher);
        //开启缓存
        realm.setCachingEnabled(true);
        realm.setAuthenticationCachingEnabled(true);
        realm.setAuthorizationCachingEnabled(true);
        return realm;
    }

    /**
     * 创建shiro web应用下的安全管理器
     * @return DefaultWebSecurityManager
     */
    @Bean
    public DefaultWebSecurityManager getSecurityManager( Realm realm){
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        securityManager.setRealm(realm);
    
        securityManager.setCacheManager(cacheManager());
        SecurityUtils.setSecurityManager(securityManager);
        return securityManager;
    }



    /**
     * 配置Redis管理器
     * @Attention 使用的是shiro-redis开源插件
     * @return
     */
    @Bean
    public RedisManager redisManager() {
        RedisManager redisManager = new RedisManager();
        redisManager.setHost(host);
        redisManager.setPort(port);
        redisManager.setTimeout(timeout);
        redisManager.setPassword(password);
        JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
        jedisPoolConfig.setMaxTotal(maxIdle+maxActive);
        jedisPoolConfig.setMaxIdle(maxIdle);
        jedisPoolConfig.setMinIdle(minIdle);
        redisManager.setJedisPoolConfig(jedisPoolConfig);
        return redisManager;
    }


    @Bean
    public RedisCacheManager cacheManager() {
        RedisCacheManager redisCacheManager = new RedisCacheManager();
        redisCacheManager.setRedisManager(redisManager());
        redisCacheManager.setKeyPrefix(CACHE_KEY);
        // shiro-redis要求放在session里面的实体类必须有个id标识
        //这是组成redis中所存储数据的key的一部分
        redisCacheManager.setPrincipalIdFieldName("username");
        return redisCacheManager;
    }

}

Modifiez la méthode doGetAuthenticationInfo dans MySQLRealm pour utiliser l'intégralité de l'objet User comme premier de SimpleAuthenticationInfo paramètres. shiro-redis obtiendra la valeur d'identification du premier paramètre dans le cadre de la clé des données dans redis en fonction de la valeur de l'attribut principalIdFieldName de RedisCacheManager. MySQLRealm中的doGetAuthenticationInfo方法,将User对象整体作为SimpleAuthenticationInfo的第一个参数。shiro-redis将根据RedisCacheManagerprincipalIdFieldName属性值从第一个参数中获取id值作为redis中数据的key的一部分。

/**
 * 认证
 * @param token
 * @return
 * @throws AuthenticationException
 */
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {

    if(token==null){
        return null;
    }
    String principal = (String) token.getPrincipal();
    User user = userService.findByUsername(principal);
    SimpleAuthenticationInfo simpleAuthenticationInfo = new MyAuthcInfo(
            //由于shiro-redis插件需要从这个属性中获取id作为redis的key
            //所有这里传的是user而不是username
            user,
            //凭证信息
            user.getPassword(),
            //加密盐值
            new CurrentSalt(user.getSalt()),
            getName());
    
    return simpleAuthenticationInfo;
}

并修改MySQLRealm中的doGetAuthorizationInfo方法,从User对象中获取主身份信息。

/**
 * 授权
 * @param principals
 * @return
 */
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
   User user = (User) principals.getPrimaryPrincipal();
    String username = user.getUsername();
    List<Role> roleList = roleService.findByUsername(username);
    SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
    for (Role role : roleList) {
        authorizationInfo.addRole(role.getRoleName());
    }
    List<Long> roleIdList  = new ArrayList<>();
    for (Role role : roleList) {
        roleIdList.add(role.getRoleId());
    }

    List<Resource> resourceList = resourceService.findByRoleIds(roleIdList);
    for (Resource resource : resourceList) {
        authorizationInfo.addStringPermission(resource.getResourcePermissionTag());
    }
    return authorizationInfo;
}
自定义Salt

由于Shiro里面默认的SimpleByteSource没有实现序列化接口,导致ByteSource.Util.bytes()生成的salt在序列化时出错,因此需要自定义Salt类并实现序列化接口。并在自定义的Realm的认证方法使用new CurrentSalt(user.getSalt())

/**由于shiro当中的ByteSource没有实现序列化接口,缓存时会发生错误
 * 因此,我们需要通过自定义ByteSource的方式实现这个接口
 * @author 赖柄沣 bingfengdev@aliyun.com
 * @version 1.0
 * @date 2020/10/8 16:17
 */
public class CurrentSalt extends SimpleByteSource implements Serializable {
    public CurrentSalt(String string) {
        super(string);
    }

    public CurrentSalt(byte[] bytes) {
        super(bytes);
    }

    public CurrentSalt(char[] chars) {
        super(chars);
    }

    public CurrentSalt(ByteSource source) {
        super(source);
    }

    public CurrentSalt(File file) {
        super(file);
    }

    public CurrentSalt(InputStream stream) {
        super(stream);
    }
}

Et modifiez la méthode doGetAuthorizationInfo dans MySQLRealm pour obtenir les informations d'identité principales de l'objet User.

/**SessionId生成器
 * <p>@author 赖柄沣 laibingf_dev@outlook.com</p>
 * <p>@date 2020/8/15 15:19</p>
 */
public class ShiroSessionIdGenerator implements SessionIdGenerator {

    /**
     *实现SessionId生成
     * @param session
     * @return
     */
    @Override
    public Serializable generateId(Session session) {
        Serializable sessionId = new JavaUuidSessionIdGenerator().generateId(session);
        return String.format("login_token_%s", sessionId);
    }
}

Sel personnalisé

Étant donné que le SimpleByteSource par défaut dans Shiro n'implémente pas l'interface de sérialisation, le sel généré par ByteSource.Util.bytes() provoque des erreurs lors de la sérialisation, une classe Salt personnalisée est donc requise Et implémentez l’interface de sérialisation. Et utilisez new CurrentSalt(user.getSalt()) pour transmettre la valeur salt dans la méthode d'authentification de domaine personnalisée.

/**
 * <p>@author 赖柄沣 laibingf_dev@outlook.com</p>
 * <p>@date 2020/8/15 15:40</p>
 */
public class ShiroSessionManager extends DefaultWebSessionManager {

    //定义常量
    private static final String AUTHORIZATION = "Authorization";
    private static final String REFERENCED_SESSION_ID_SOURCE = "Stateless request";
    //重写构造器
    public ShiroSessionManager() {
        super();
        this.setDeleteInvalidSessions(true);
    }

    /**
     * 重写方法实现从请求头获取Token便于接口统一
     *      * 每次请求进来,
     *      Shiro会去从请求头找Authorization这个key对应的Value(Token)
     * @param request
     * @param response
     * @return
     */
    @Override
    public Serializable getSessionId(ServletRequest request, ServletResponse response) {
        String token = WebUtils.toHttp(request).getHeader(AUTHORIZATION);
        //如果请求头中存在token 则从请求头中获取token
        if (!StringUtils.isEmpty(token)) {
            request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_SOURCE, REFERENCED_SESSION_ID_SOURCE);
            request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID, token);
            request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_IS_VALID, Boolean.TRUE);
            return token;
        } else {
            // 这里禁用掉Cookie获取方式
            return null;
        }
    }
}

Ajouter une session personnalisée Shiro

Ajouter un générateur d'ID de session personnalisé

/**
 * SessionID生成器
 *
 */
@Bean
public ShiroSessionIdGenerator sessionIdGenerator(){
    return new ShiroSessionIdGenerator();
}

/**
 * 配置RedisSessionDAO
 */
@Bean
public RedisSessionDAO redisSessionDAO() {
    RedisSessionDAO redisSessionDAO = new RedisSessionDAO();
    redisSessionDAO.setRedisManager(redisManager());
    redisSessionDAO.setSessionIdGenerator(sessionIdGenerator());
    redisSessionDAO.setKeyPrefix(SESSION_KEY);
    redisSessionDAO.setExpire(EXPIRE);
    return redisSessionDAO;
}

/**
 * 配置Session管理器
 * @Author Sans
 *
 */
@Bean
public SessionManager sessionManager() {
    ShiroSessionManager shiroSessionManager = new ShiroSessionManager();
    shiroSessionManager.setSessionDAO(redisSessionDAO());
    //禁用cookie
    shiroSessionManager.setSessionIdCookieEnabled(false);
    //禁用会话id重写
    shiroSessionManager.setSessionIdUrlRewritingEnabled(false);
    return shiroSessionManager;
}

Ajouter un gestionnaire de session personnalisé

/**
 * 认证
 * @param token
 * @return
 * @throws AuthenticationException
 */
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {

    if(token==null){
        return null;
    }
    String principal = (String) token.getPrincipal();
    User user = userService.findByUsername(principal);
    SimpleAuthenticationInfo simpleAuthenticationInfo = new MyAuthcInfo(
            //由于shiro-redis插件需要从这个属性中获取id作为redis的key
            //所有这里传的是user而不是username
            user,
            //凭证信息
            user.getPassword(),
            //加密盐值
            new CurrentSalt(user.getSalt()),
            getName());

    //清除当前主体旧的会话,相当于你在新电脑上登录系统,把你之前在旧电脑上登录的会话挤下去
    ShiroUtils.deleteCache(user.getUsername(),true);
    return simpleAuthenticationInfo;
}

Configurer un gestionnaire de session personnalisé

Ajouter la configuration du gestionnaire de session dans ShiroConfig

@PostMapping("/login")
public HashMap<Object, Object> login(@RequestBody LoginVO loginVO) throws AuthenticationException {
    boolean flags = authcService.login(loginVO);
    HashMap<Object, Object> map = new HashMap<>(3);
    if (flags){
        Serializable id = SecurityUtils.getSubject().getSession().getId();
        map.put("msg","登录成功");
        map.put("token",id);
        return map;
    }else {
        return null;
    } 
}

Dans la dernière version (1.6.0), la configuration setSessionIdUrlRewritingEnabled(false) du gestionnaire de session ne prend pas effet, ce qui entraîne plusieurs erreurs de redirection lors de l'accès direct aux ressources protégées sans authentification. Ce bug a été résolu après le passage de la version Shiro à la 1.5.0.

À l'origine, cet article devait être publié hier soir. Pour cette raison, il m'a fallu beaucoup de temps pour le publier aujourd'hui. . .

Modifiez la méthode d'authentification doGetAuthenticationInfo du domaine personnalisé

Avant que les informations d'authentification ne soient renvoyées, nous devons porter un jugement : si l'utilisateur actuel s'est connecté sur l'ancien appareil, l'identifiant de session sur l'ancien appareil doit être supprimé et il est hors ligne.

/**shiro异常处理
 * @author 赖柄沣 bingfengdev@aliyun.com
 * @version 1.0
 * @date 2020/10/7 18:01
 */
@ControllerAdvice(basePackages = "pers.lbf.springbootshiro")
public class AuthExceptionHandler {

    //==================认证异常====================//

    @ExceptionHandler(ExpiredCredentialsException.class)
    @ResponseBody
    public String expiredCredentialsExceptionHandlerMethod(ExpiredCredentialsException e) {
        return "凭证已过期";
    }

    @ExceptionHandler(IncorrectCredentialsException.class)
    @ResponseBody
    public String incorrectCredentialsExceptionHandlerMethod(IncorrectCredentialsException e) {
        return "用户名或密码错误";
    }

    @ExceptionHandler(UnknownAccountException.class)
    @ResponseBody
    public String unknownAccountExceptionHandlerMethod(IncorrectCredentialsException e) {
        return "用户名或密码错误";
    }

    
    @ExceptionHandler(LockedAccountException.class)
    @ResponseBody
    public String lockedAccountExceptionHandlerMethod(IncorrectCredentialsException e) {
        return "账户被锁定";
    }

    //=================授权异常=====================//

    @ExceptionHandler(UnauthorizedException.class)
    @ResponseBody
    public String unauthorizedExceptionHandlerMethod(UnauthorizedException e){
        return "未授权!请联系管理员授权";
    }
}

Modifier l'interface de connexion

Nous stockons les informations de session dans Redis et renvoyons l'ID de session à l'utilisateur sous la forme d'un jeton une fois l'utilisateur authentifié. L'utilisateur apporte ce jeton lors de la demande de ressources protégées. Nous obtenons les informations d'autorisation de l'utilisateur auprès de redis sur la base des informations du jeton pour effectuer le contrôle d'accès.

rrreee

Ajouter une gestion globale des exceptions

rrreee

Dans le développement réel, les résultats renvoyés doivent être unifiés et les codes d'erreur métier doivent être donnés. Cela dépasse le cadre de cet article. Si nécessaire, veuillez le considérer en fonction des caractéristiques de votre propre système.

Test

Authentification

Comment Springboot implémente lauthentification et la gestion dynamique des autorisations

Connexion réussie🎜🎜🎜🎜🎜

Nom d'utilisateur ou mot de passe incorrect

Comment Springboot implémente lauthentification et la gestion dynamique des autorisations

Pour des raisons de sécurité, ne révélez pas si le nom d'utilisateur ou le mot de passe est erroné.

Accès aux ressources protégées

Accès aux ressources autorisées après authentification

Comment Springboot implémente lauthentification et la gestion dynamique des autorisations

Accès aux ressources non autorisées après authentification

Comment Springboot implémente lauthentification et la gestion dynamique des autorisations

Accès direct sans authentification

Comment Springboot implémente lauthentification et la gestion dynamique des autorisations

Voir Les trois valeurs clés de redis

Comment Springboot implémente lauthentification et la gestion dynamique des autorisations

correspondent respectivement au cache d'informations d'authentification, au cache d'informations d'autorisation et au cache d'informations de session.

Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!

Déclaration:
Cet article est reproduit dans:. en cas de violation, veuillez contacter admin@php.cn Supprimer