SpringBoot-Shiro

Shiro

1. QuickStart

1. 导入依赖

这里同时导入了log4j, Shiro默认的日志是commons-logging

<dependencies>
    <dependency>
        <groupId>org.apache.shiro</groupId>
        <artifactId>shiro-core</artifactId>
        <version>1.6.0</version>
    </dependency>

    <!-- configure logging -->
    <dependency>
        <groupId>org.slf4j</groupId>
        <artifactId>jcl-over-slf4j</artifactId>
        <scope>runtime</scope>
        <version>1.7.21</version>
    </dependency>
    <dependency>
        <groupId>org.slf4j</groupId>
        <artifactId>slf4j-log4j12</artifactId>
        <scope>runtime</scope>
        <version>1.7.21</version>
    </dependency>
    <dependency>
        <groupId>log4j</groupId>
        <artifactId>log4j</artifactId>
        <scope>runtime</scope>
        <version>1.2.17</version>
    </dependency>
</dependencies>

2. 配置shiro

1. log4j配置

log4j.rootLogger=INFO, stdout

log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%d %p [%c] - %m %n

# General Apache libraries
log4j.logger.org.apache=WARN

# Spring
log4j.logger.org.springframework=WARN

# Default Shiro logging
log4j.logger.org.apache.shiro=INFO

# Disable verbose logging
log4j.logger.org.apache.shiro.util.ThreadContext=WARN
log4j.logger.org.apache.shiro.cache.ehcache.EhCache=WARN

2. shiro配置

#
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements.  See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership.  The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License.  You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied.  See the License for the
# specific language governing permissions and limitations
# under the License.
#
# =============================================================================
# Quickstart INI Realm configuration
#
# For those that might not understand the references in this file, the
# definitions are all based on the classic Mel Brooks' film "Spaceballs". ;)
# =============================================================================

# -----------------------------------------------------------------------------
# Users and their assigned roles
#
# Each line conforms to the format defined in the
# org.apache.shiro.realm.text.TextConfigurationRealm#setUserDefinitions JavaDoc
# -----------------------------------------------------------------------------
[users]
# user 'root' with password 'secret' and the 'admin' role
root = secret, admin
# user 'guest' with the password 'guest' and the 'guest' role
guest = guest, guest
# user 'presidentskroob' with password '12345' ("That's the same combination on
# my luggage!!!" ;)), and role 'president'
presidentskroob = 12345, president
# user 'darkhelmet' with password 'ludicrousspeed' and roles 'darklord' and 'schwartz'
darkhelmet = ludicrousspeed, darklord, schwartz
# user 'lonestarr' with password 'vespa' and roles 'goodguy' and 'schwartz'
lonestarr = vespa, goodguy, schwartz

# -----------------------------------------------------------------------------
# Roles with assigned permissions
#
# Each line conforms to the format defined in the
# org.apache.shiro.realm.text.TextConfigurationRealm#setRoleDefinitions JavaDoc
# -----------------------------------------------------------------------------
[roles]
# 'admin' role has all permissions, indicated by the wildcard '*'
admin = *
# The 'schwartz' role can do anything (*) with any lightsaber:
schwartz = lightsaber:*
# The 'goodguy' role is allowed to 'drive' (action) the winnebago (type) with
# license plate 'eagle5' (instance specific id)
goodguy = winnebago:drive:eagle5

3. QuickStart

import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.*;
import org.apache.shiro.config.IniSecurityManagerFactory;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.session.Session;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.util.Factory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class Quickstart {

    private static final transient Logger log = LoggerFactory.getLogger(Quickstart.class);


    public static void main(String[] args) {

        // The easiest way to create a Shiro SecurityManager with configured
        // realms, users, roles and permissions is to use the simple INI config.
        // We'll do that by using a factory that can ingest a .ini file and
        // return a SecurityManager instance:

        // Use the shiro.ini file at the root of the classpath
        // (file: and url: prefixes load from files and urls respectively):
        Factory<SecurityManager> factory = new IniSecurityManagerFactory("classpath:shiro.ini");
        SecurityManager securityManager = factory.getInstance();

        // for this simple example quickstart, make the SecurityManager
        // accessible as a JVM singleton.  Most applications wouldn't do this
        // and instead rely on their container configuration or web.xml for
        // webapps.  That is outside the scope of this simple quickstart, so
        // we'll just do the bare minimum so you can continue to get a feel
        // for things.
        SecurityUtils.setSecurityManager(securityManager);

        // Now that a simple Shiro environment is set up, let's see what you can do:

        // get the currently executing user:
        //获取当前的用户对象 Subject
        Subject currentUser = SecurityUtils.getSubject();

        // Do some stuff with a Session (no need for a web or EJB container!!!)
        //通过当前用户拿到 session (可以脱离Web)
        Session session = currentUser.getSession();
        session.setAttribute("someKey", "aValue");
        String value = (String) session.getAttribute("someKey");
        if (value.equals("aValue")) {
            log.info("Retrieved the correct value! [" + value + "]");
        }

        // let's login the current user so we can check against roles and permissions:
        //判断当前的用户是否被认证
        if (!currentUser.isAuthenticated()) {
            //Token :   令牌, 此处没有获取, 随机设置
            UsernamePasswordToken token = new UsernamePasswordToken("lonestarr", "vespa");
            //设置记住我
            token.setRememberMe(true);
            try {
                //执行登录操作
                currentUser.login(token);
            } catch (UnknownAccountException uae) {
                log.info("There is no user with username of " + token.getPrincipal());
            } catch (IncorrectCredentialsException ice) {
                log.info("Password for account " + token.getPrincipal() + " was incorrect!");
            } catch (LockedAccountException lae) {
                log.info("The account for username " + token.getPrincipal() + " is locked.  " +
                        "Please contact your administrator to unlock it.");
            }
            // ... catch more exceptions here (maybe custom ones specific to your application?
            //认证异常(总的异常)
            catch (AuthenticationException ae) {
                //unexpected condition?  error?
            }
        }

        //say who they are:
        //print their identifying principal (in this case, a username):
        log.info("User [" + currentUser.getPrincipal() + "] logged in successfully.");

        //test a role:
        if (currentUser.hasRole("schwartz")) {
            log.info("May the Schwartz be with you!");
        } else {
            log.info("Hello, mere mortal.");
        }

        //test a typed permission (not instance-level)
        if (currentUser.isPermitted("lightsaber:wield")) {
            log.info("You may use a lightsaber ring.  Use it wisely.");
        } else {
            log.info("Sorry, lightsaber rings are for schwartz masters only.");
        }

        //a (very powerful) Instance Level permission:
        if (currentUser.isPermitted("winnebago:drive:eagle5")) {
            log.info("You are permitted to 'drive' the winnebago with license plate (id) 'eagle5'.  " +
                    "Here are the keys - have fun!");
        } else {
            log.info("Sorry, you aren't allowed to drive the 'eagle5' winnebago!");
        }

        //all done - log out!
        //注销
        currentUser.logout();

        //结束启动
        System.exit(0);
    }
}

2. SpringBoot中集成

Shiro三大核心

  • Subject: 用户
  • SecurityManager: 管理所有用户
  • Realm: 连接数据

1. 导入springboot整合shiro的包

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

2. 自定义UserRealm

package com.wang.config;

import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;

//自定义的 UserRealm, 继承AuthorizingRealm即可
public class UserRealm extends AuthorizingRealm {

    //授权
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        System.out.println("执行了 => AuthorizationInfo 授权");
        return null;
    }

    //认证
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        System.out.println("执行了 => AuthenticationInfo 认证");
        return null;
    }
}

注意

  • 想要自定义Realm, 继承AuthorizingRealm即可
  • 要重写抽象类AuthorizationInfo和AuthenticationInfo

3. 配置Shiro

package com.wang.config;

import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.LinkedHashMap;

@Configuration
public class ShiroConfig {

    //ShiroFilterFactoryBean : 3
    @Bean
    public ShiroFilterFactoryBean shiroFilterFactoryBean(@Qualifier("getDefaultWebSecurityManager") DefaultWebSecurityManager defaultWebSecurityManager) {
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();

        //关联SecurityManager, 设置安全管理器
        shiroFilterFactoryBean.setSecurityManager(defaultWebSecurityManager);

        //添加shiro的内置过滤器
        /*
            anon: 无需认证, 就可以访问
            authc: 必须认证了才能访问
            user: 必须拥有 记住我 功能才能访问
            perms: 拥有对于某个资源的权限才能访问
            role: 拥有某个角色权限才能访问
         */
        LinkedHashMap<String, String> filterChainDefinitionMap = new LinkedHashMap<>();

//        filterChainDefinitionMap.put("/user/add", "authc");
//        filterChainDefinitionMap.put("/user/update", "authc");
        filterChainDefinitionMap.put("/user/*", "authc");

        shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);

        //设置登录的请求
        shiroFilterFactoryBean.setLoginUrl("/toLogin");


        return shiroFilterFactoryBean;
    }

    //DefaultWebSecurityManager : 2
    //@Qualifier() 利用bean的id注入, 在注解托管中即为方法名
    @Bean
    public DefaultWebSecurityManager getDefaultWebSecurityManager(@Qualifier("userRealm") UserRealm userRealm) {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();

        //关联UserRealm
        securityManager.setRealm(userRealm);

        return securityManager;
    }

    //创建realm对象, 需要自定义 : 1
    //将自己定义的Realm注册为Bean, 被SpringBoot托管
    @Bean
    public UserRealm userRealm() {
        return new UserRealm();
    }
}

注意:

  • 不要忘记注册Bean
  • @Qualifier() 利用bean的id注入, 在注解托管中即为方法名
  • 实际写法的过程应该为: UserRealm --> DefaultWebSecurityManager --> ShiroFilterFactoryBean
  • Shiro过滤器的通过k-v的方法放入参数, 有以下五个参数
    • anon: 无需认证, 就可以访问

    • authc: 必须认证了才能访问

    • user: 必须拥有 记住我 功能才能访问

    • perms: 拥有对于某个资源的权限才能访问

    • role: 拥有某个角色权限才能访问

4. Controller

package com.wang.controller;

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller
public class MyController {

    @RequestMapping({"/", "/index"})
    public String toIndex(Model model) {
        model.addAttribute("msg", "Hello, Shiro!");
        return "index";
    }

    @RequestMapping("/user/add")
    public String add() {
        return "user/add";
    }

    @RequestMapping("/user/update")
    public String update() {
        return "user/update";
    }

    @RequestMapping("/toLogin")
    public String toLogin() {
        return "login";
    }
}

前端页面代码略

3. Shiro用户认证

1. 用户登录判断

这里写在controller中, 利用不同的结果输出提示信息并进行跳转

@RequestMapping("/login")
public String login(String username, String password, Model model) {
    //获取当前的用户
    Subject subject = SecurityUtils.getSubject();
    //封装用户的登录数据
    UsernamePasswordToken token = new UsernamePasswordToken(username, password);
    //执行登录的方法, 如果没有异常, 说明登录OK
    try {
        subject.login(token);
        return "index";
    } catch (UnknownAccountException e) {
        //用户名不存在
        model.addAttribute("msg", "用户名错误");
        return "login";
    } catch (IncorrectCredentialsException e) {
        //密码不存在
        model.addAttribute("msg", "密码错误");
        return  "login";
    }

注意

  • 通过Subject对象获得当前的用户 ==> Subject subject = SecurityUtils.getSubject();
  • 要将用户的信息封装在token中
  • subject.login(token); ==> 即可进行用户登录
  • 判断用户认证结果, 这里通过捕获不同的异常进行!

2. 用户认证配置

在自定义的UserRealm中, 进行配置

//认证
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
    System.out.println("执行了 => AuthenticationInfo 认证");

    //用户名, 密码 ==> 数据库中取
    String name = "root";
    String password = "123456";

    UsernamePasswordToken userToken = (UsernamePasswordToken) authenticationToken;

    //用户名认证
    if (!userToken.getUsername().equals(name)) {
        //抛出异常 UnknownAccountException
        return null;
    }

    //密码认证 : shiro做
    return new SimpleAuthenticationInfo("", password, "");

}

注意

  • 传入的参数即为token, 要进行类型转换 ==> UsernamePasswordToken
  • 对token进行操作即可配置, 返回值为null为对应的方法的异常
  • 密码认证shiro做, 不需要我们进行操作, 只需要返回SimpleAuthenticationInfo, 其中的参数传递password等

4. 整合Shiro与Mybatis和Thymeleaf

1. 导入依赖

Shiro不需要导入与Mybatis的整合, 只需要导入与thymeleaf的整合包

<!--shiro整合thymeleaf-->
<!-- https://mvnrepository.com/artifact/com.github.theborakompanioni/thymeleaf-extras-shiro -->
<dependency>
   <groupId>com.github.theborakompanioni</groupId>
   <artifactId>thymeleaf-extras-shiro</artifactId>
   <version>2.0.0</version>
</dependency>

2. 配置Shiro

package com.wang.config;

import at.pollux.thymeleaf.shiro.dialect.ShiroDialect;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.LinkedHashMap;

@Configuration
public class ShiroConfig {

    //ShiroFilterFactoryBean : 3
    @Bean
    public ShiroFilterFactoryBean shiroFilterFactoryBean(@Qualifier("getDefaultWebSecurityManager") DefaultWebSecurityManager defaultWebSecurityManager) {
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();

        //关联SecurityManager, 设置安全管理器
        shiroFilterFactoryBean.setSecurityManager(defaultWebSecurityManager);

        //添加shiro的内置过滤器
        /*
            anon: 无需认证, 就可以访问
            authc: 必须认证了才能访问
            user: 必须拥有 记住我 功能才能访问
            perms: 拥有对于某个资源的权限才能访问
            role: 拥有某个角色权限才能访问
         */
        //拦截
        LinkedHashMap<String, String> filterChainDefinitionMap = new LinkedHashMap<>();

//        filterChainDefinitionMap.put("/user/add", "authc");
//        filterChainDefinitionMap.put("/user/update", "authc");
        //授权
        filterChainDefinitionMap.put("/user/add", "perms[user:add]");
        filterChainDefinitionMap.put("/user/update", "perms[user:update]");

        filterChainDefinitionMap.put("/user/*", "authc");

        shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);

        //设置登录的请求
        shiroFilterFactoryBean.setLoginUrl("/toLogin");
        //未授权页面
        shiroFilterFactoryBean.setUnauthorizedUrl("/noauth");


        return shiroFilterFactoryBean;
    }

    //DefaultWebSecurityManager : 2
    //@Qualifier() 利用bean的id注入, 在注解托管中即为方法名
    @Bean
    public DefaultWebSecurityManager getDefaultWebSecurityManager(@Qualifier("userRealm") UserRealm userRealm) {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();

        //关联UserRealm
        securityManager.setRealm(userRealm);

        return securityManager;
    }

    //创建realm对象, 需要自定义 : 1
    //将自己定义的Realm注册为Bean, 被SpringBoot托管
    @Bean
    public UserRealm userRealm() {
        return new UserRealm();
    }

    //整合ShiroDialect: 用来整合 shiro thymeleaf
    @Bean
    public ShiroDialect shiroDialect() {
        return new ShiroDialect();
    }
}

注意

  • 配置中的拦截器, 登录请求都在ShiroFilterFactoryBean中配置, 这是由于在Shiro运行时, 他是最后一层直接和用户交互的!
  • setFilterChainDefinitionMap方法以K-V的方式储存过滤器的URL和权限信息
    • perms资源过滤器要写成perms[XXX], XXX为子字符串, 可以在数据库中储存对应的权限字段, 这里是比对的标准
  • 要整合Thymeleaf, 就要注册ShiroDialect这个Bean
  • 配置中不涉及数据库的操作

3. 在自定义的Realm中配置授权和认证

package com.wang.config;

import com.wang.pojo.User;
import com.wang.service.UserServiceImpl;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.*;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.session.Session;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.subject.Subject;
import org.springframework.beans.factory.annotation.Autowired;

//自定义的 UserRealm, 继承AuthorizingRealm即可
public class UserRealm extends AuthorizingRealm {

    @Autowired
    private UserServiceImpl userService;

    //授权
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        System.out.println("执行了 => AuthorizationInfo 授权");

        SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();

        //拿到当前登录的这个对象, user在下面已经放到了subject中
        Subject subject = SecurityUtils.getSubject();
        //拿到user对象
        User currentUser = (User) subject.getPrincipal();
        //放到perms中, 在ShiroConfig中以K-V调用
        info.addStringPermission(currentUser.getPerms());

        return info;
    }

    //认证
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        System.out.println("执行了 => AuthenticationInfo 认证");

        //用户名, 密码 ==> 数据库中取
        //从token中获得用户名
        UsernamePasswordToken userToken = (UsernamePasswordToken) authenticationToken;

        String username = userToken.getUsername();
        User user = userService.queryUserByName(username);

        //输入的用户名在数据库中不存在
        if (user == null) {
            return null;
        }

        Subject currentSubject = SecurityUtils.getSubject();
        Session session = currentSubject.getSession();
        session.setAttribute("loginUser", user);

        String password = user.getPwd();

        //密码认证 : shiro做(password与token从前端取出的密码进行比较)
        //此处传入一个user, 放到了Subject中
        return new SimpleAuthenticationInfo(user, password, "");

    }
}

注意

  • 授权

    • 此处要和数据库交互, 获得用户的信息以及权限, 只需要自动装配Service的实现类即可
    • 通过 SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();创建一个授权的对象
    • 通过Subject subject = SecurityUtils.getSubject();拿到当前的对象, 进行授权操作
    • 通过User currentUser = (User) subject.getPrincipal(); 可以将Subject的字段和User实体类一一对应
    • 通过 info.addStringPermission(currentUser.getPerms()); 将User中的权限的字段放到Shiro权限验证中(Perms["XXX"])
    • 注意要返回授权的对象
  • 认证

    • 用户名和密码已经放在了token中 UsernamePasswordToken userToken = (UsernamePasswordToken) authenticationToken; 这个token是从controller层中利用从前端的数据封装好的

    • 这里可以设置Subject对象的session, 存放一个loginUser字段, 判断是否有User从而选择是否在前端显示登陆的链接, 这里的session不是Web中的session, 是Shiro自带的, 全局都能用(和web的session差不多)

    • SimpleAuthenticationInfo对象同样的是返回我们设置完的认证对象, 参数设置如下

      • principal   the 'primary' principal associated with the specified realm.
        credentials the credentials that verify the given principal.
        realmName   the realm from where the principal and credentials were acquired.
        public SimpleAuthenticationInfo(Object principal, Object credentials, String realmName)
        
    • 此处的用户名, 密码, 以及用户对象都从数据库中取出

    • 此处的放回值 return new SimpleAuthenticationInfo(user, password, ""); 里面传递了我们从数据库中取出的user, 在授权时由于关联了字段, 因此Shiro会帮我们从user中取出perms对应的字段, 和config中的过滤器进行比较

4. Controller

@RequestMapping("/login")
public String login(String username, String password, Model model) {
    //获取当前的用户
    Subject subject = SecurityUtils.getSubject();
    //封装用户的登录数据
    UsernamePasswordToken token = new UsernamePasswordToken(username, password);
    //执行登录的方法, 如果没有异常, 说明登录OK
    try {
        subject.login(token);
        return "index";
    } catch (UnknownAccountException e) {
        //用户名不存在
        model.addAttribute("msg", "用户名错误");
        return "login";
    } catch (IncorrectCredentialsException e) {
        //密码不存在
        model.addAttribute("msg", "密码错误");
        return  "login";
    }
}

注意

  • Controller层中一定要接收前端的username和password, 并通过UsernamePasswordToken对象给Shiro一个token从而实现Realm层的调用验证
  • 同样的, 要获取当前用户的Subject, 直接调用login方法, 放入token即可进行登录的验证, 通过不同的异常类型进行提示和跳转

5. thymeleaf

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org"
      xmlns:shiro="http://www.pollix.at/thymeleaf/shiro">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>

<h1>首页</h1>
<p>
    <span th:text="${msg}"></span>
</p>
<p th:if="${session.loginUser==null}">
    <a th:href="@{/toLogin}">登录</a>
</p>

<hr>

<div shiro:hasPermission="user:add">
    <a th:href="@{/user/add}">add</a>
</div>
<div shiro:hasPermission="user:update">
    <a th:href="@{/user/update}">update</a>
</div>


</body>
</html>

注意

  • 要引入命名空间 xmlns:shiro="http://www.pollix.at/thymeleaf/shiro"
  • shiro整合的格式类似thymeleaf, 以shiro:开头, 后面跟属性名
  • shiro:hasPermission="XXX"进行权限控制, XXX为我们在config中设定的权限的属性名
  • 此处的session为我们在Shiro中定义的session, 里面存放了我们自定义的属性以及值
原文地址:https://www.cnblogs.com/wang-sky/p/13728638.html