一、Shiro角色管理

1.1 模拟数据

角色:不同意味着不同权限
示例代码:
在main.html中添加三个超链接,分别访问三个不同的HTML页面

  1. <body>
  2. <p>用户的后台管理页面</p>
  3. <a href="/admin.html">管理员能访问的页面</a><br>
  4. <a href="/teacher.html">teacher能访问的</a><br>
  5. <a href="/student.html">student能访问的</a><br>
  6. <button id="logout">注销</button>
  7. </body>

在static下创建这三个页面

1.1.1 shiro配置类

在过滤器方法中添加角色配置
注意:roles后[]中的角色信息是程序员自定义的,其中,OrFilter需要程序员手写,并且需要配置

//role 角色,当请求相关url时,匹配到的过滤器用roles表示
//访问该URL必须是[]中指定的角色
//[]中的名字是程序员自定义的
//当shiro识别到roles过滤器时会自动调用realm中获取授权信息的方法
map.put("/admin.html","roles[admin]");
map.put("/teacher.html","roles[teacher]");
map.put("/student.html","roles[student]");
//必须同时为admin和teacher才可访问
map.put("/adminAndTeacher.html","roles[admin,teacher]");
//自定义过滤器OrFilter:表示只要具备其中一个角色都可以访问
map.put("/adminOrTeacher.html","or[admin,teacher]");

创建过滤器bean

//4.创建自定义过滤器bean
@Bean
public OrFilter orFilter(){
    return new OrFilter();
}

将过滤器注册到过滤器factoryBean中

    @Bean
    public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager,OrFilter orFilter){
        ShiroFilterFactoryBean factoryBean = new ShiroFilterFactoryBean();
        //5.注册自定义的过滤器
        //javax.servlet.Filter
        Map<String, Filter> filterMap = new HashMap<>();
        filterMap.put("or",orFilter);
        factoryBean.setFilters(filterMap);

        //............此处代码省略.................

        return factoryBean;
    }

1.1.2 OrFilter过滤器

import org.apache.shiro.SecurityUtils;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.web.filter.authz.AuthorizationFilter;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;

public class OrFilter extends AuthorizationFilter {

    //返回值为true表示能够访问,返回值为false表示不允许访问
    @Override
    protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception {
        //1.获取Subject
        Subject currentUser = SecurityUtils.getSubject();

        //2.获取到过滤器中设置的角色信息
        String[] roles = (String[]) mappedValue;

        //3.判断
        if(roles!=null && roles.length>0){
            for (String role : roles) {
                if (currentUser.hasRole(role)){
                    return true;
                }
            }
        }
        return false;
    }
}

1.1.3 Realm类

在realm获取授权信息方法中模拟从数据库中获取到角色信息,并封装返回

@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
    logger.info("获取授权信息(角色、权限)");
    //获取账号信息
    String account = (String) principals.getPrimaryPrincipal();
    //模拟从数据库中获取到了当前用户的角色信息
    Set<String> roles = new HashSet<>();
    roles.add("admin");
    roles.add("teacher");
    roles.add("student");
    //封装角色信息
    SimpleAuthorizationInfo info = new SimpleAuthorizationInfo(roles);
    //
    return info;//返回给securityManger(subject)
}

1.2 数据库中获取数据

创建表格

CREATE TABLE role(
    id  INT PRIMARY KEY AUTO_INCREMENT,
    `name` VARCHAR(20)
);
CREATE TABLE user_role(
    uid INT,
    rid INT
);

user_role
image.png
role
image.png

1.2.1 Role实体类

@Data
public class Role {
    private int id;
    private String name;
}

1.2.2 修改User实体类

@Data
@Accessors(chain = true)
public class User {
    private int id;
    private String account;
    private String pwd;
    private List<Role> roles;
}

1.2.3 RoleMapper

@Mapper
@Repository
public interface RoleMapper {
    //通过用户id去role、user_role做连表查询
    @Select("select * from user_role ur,role r where ur.rid=r.id and ur.uid = #{uid}")
    public List<Role> findRolesByUid(int uid);
}

1.2.4 UserMapper

//通过账号查询用户的信息及角色信息
@Select("select * from user where account = #{account}")
@Results({
   @Result(id = true,column = "id",property = "id"),
   @Result(column = "id",property = "roles"
       ,many = @Many(select = "com.woniuxy.shiro.mapper.RoleMapper.findRolesByUid"))
})
public User findUserAndRolesByAccount(String account);

1.2.5 修改Realm类

    private static final Logger logger = LoggerFactory.getLogger(CustomRealm.class);
    /*
     * 获取权限信息
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        logger.info("获取授权的信息(角色、权限)");
        //获取账号信息
        String account = (String) principals.getPrimaryPrincipal();
        //以账号作为查询条件,查询出当前用户的所有角色    权限管理系统中一般都存在一个账号多个角色的情况
        User user = userService.findUserAndRolesByAccount(account);
        logger.info(user.toString());

        Set<String> roles = new HashSet<>();
        //遍历获取角色信息
        for (Role role : user.getRoles()) {
            roles.add(role.getName());
        }
        //封装角色信息
        SimpleAuthorizationInfo info = new SimpleAuthorizationInfo(roles);
        //返回给securityManager(subject)
        return info;
    }

二、Shiro权限管理

2.1 模拟数据

2.1.1 shiro配置类

在过滤器方法中添加权限配置

//权限配置 permissions:权限
map.put("/teacher/add","perms[teacher:add]");

2.1.2 TeacherController

@RestController
@RequestMapping("/teacher")
public class TeacherController extends ExceptionCenter{
    private static final Logger logger = LoggerFactory.getLogger(TeacherController.class);
    @RequestMapping("/add")
    public String add(){
        logger.info("添加教师信息");
        return "success";
    }
}

2.1.3 修改Realm类

修改realm获取授权信息的方法,模拟从数据库中获取权限信息

    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        logger.info("获取授权信息(角色、权限)");
        //获取账号信息
        String account = (String) principals.getPrimaryPrincipal();
        //以账号作为查询条件查询出当前用户的所有角色    权限管理系统一般都存在一个账号多个角色情况
        User user = userService.findUserAndRolesByAccount(account);
        //模拟从数据库中获取到了当前用户的角色信息
        Set<String> roles = new HashSet<>();
        //遍历获取角色、权限信息
        for (Role role : user.getRoles()) {
            roles.add(role.getName());
        }
        //假设从数据库中获取到了权限信息
        Set<String> perms = new HashSet<>();
        //perms.add("teacher:add");
        perms.add("teacher:del");
        perms.add("teacher:update");
        perms.add("teacher:find");
        //封装角色信息
        SimpleAuthorizationInfo info = new SimpleAuthorizationInfo(roles);
        //封装权限信息
        info.setStringPermissions(perms);
        //
        return info;//返回给securityManger(subject)
    }

2.2 数据库中获取数据

2.2.1 Perm实体类

@Data
public class Perm {
    private int id;
    private String name;
}

2.2.2 修改Role实体类

@Data
public class Role {
    private int id;
    private String name;
    private List<Perm> perms;
}

2.2.3 PermMapper

@Mapper
@Repository
public interface PermMapper {
    //根据rid联表查询
    @Select("select * from role_perms rp,perms p where rp.pid = p.id and rp.rid=#{rid}")
    public List<Perm> findPermsByRid(int rid);
}

2.2.4 修改RoleMapper

@Mapper
@Repository
public interface RoleMapper {
    //通过用户id去role、user_role联表查询
    @Select("select * from user_role ur,role r where ur.rid = r.id and ur.uid = #{uid}")
    @Results({
            @Result(id = true,column = "rid",property = "id"),
            @Result(column = "rid",property = "perms"
                    ,many = @Many(select = "com.woniuxy.shiro.mapper.PermMapper.findPermsByRid"))
    })
    public  List<Role> findRolesByUid(int uid);
}

2.2.5 修改Realm类

修改realm获取授权信息方法

    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        logger.info("获取授权的信息(角色、权限)");
        //获取账号信息
        String account = (String) principals.getPrimaryPrincipal();
        //以账号作为查询条件,查询出当前用户的所有角色    权限管理系统中一般都存在一个账号多个角色的情况
        User user = userService.findUserAndRolesByAccount(account);
        logger.info(user.toString());

        Set<String> roles = new HashSet<>();
        Set<String> perms = new HashSet<>();
        //遍历获取角色、权限信息
        for (Role role : user.getRoles()) {
            roles.add(role.getName());
            for (Perm perm : role.getPerms()) {
                perms.add(perm.getName());
            }
        }
        //封装角色信息
        SimpleAuthorizationInfo info = new SimpleAuthorizationInfo(roles);
        //封装权限信息
        info.setStringPermissions(perms);
        //返回给securityManager(subject)
        return info;
    }

三、注解开发

shiro支持注解方式进行开发,但是没有提供在主启动类上使用注解开启shiro的注解扫描,需要自己在shiro配置类中配置

3.1 shiro配置类

    //6.配置shiro注解的支持:shiro注解基于AOP实现
    //6.1 配置通知
    @Bean
    public AuthorizationAttributeSourceAdvisor advisor(SecurityManager securityManager){
        AuthorizationAttributeSourceAdvisor sourceAdvisor =
                new AuthorizationAttributeSourceAdvisor();
        sourceAdvisor.setSecurityManager(securityManager);
        return sourceAdvisor;
    }
    //6.2 配置代理器
    @Bean
    public DefaultAdvisorAutoProxyCreator creator(){
        DefaultAdvisorAutoProxyCreator proxyCreator =
                new DefaultAdvisorAutoProxyCreator();
        //开启目标代理
        proxyCreator.setProxyTargetClass(true);
        return proxyCreator;
    }

3.2 TeacherController中添加方法测试

    //or或者关系,and并且关系
    //设置角色权限
    @RequiresRoles(value = {"admin","teacher"},logical = Logical.OR)
    //设置权限
//    @RequiresPermissions({"teacher:del"})
    @RequestMapping("/del")
    public String del(){
        logger.info("删除教师信息");
        return "success";
    }

运行程序,访问url,报500错误,表明注解生效,但是shiro没有提供处理注解报错的方法,需要自己通过异常处理解析器进行处理

3.3 异常解析器父类

public class ExceptionCenter {
    @ExceptionHandler({Exception.class})
    public ResponseResult<String> handleException(Exception e){
        ResponseResult<String> result = new ResponseResult<>();
        if(e instanceof AuthorizationException){
            result.setCode(500);
            result.setMessage("你没有权限!");
            result.setStatus("NO_AUTHORIZED");
        }
        return result;
    }
}

让TeacherController继承该类

public class TeacherController extends ExceptionCenter{}

四、Session管理

shiro也为用户提供一个session,程序员可以通过该session在任何一层获取到session中的数据
与HttpSession有所不同,具体见下图
57 - Shiro - 角色、权限管理,session管理,shiro标签,remember me - 图3

4.1 Session存取数据

4.1.1 获取Session

该session可以通过subject对象获取

4.1.2 存储数据

UserController中向Session存储数据

    //1.获取到“当前用户”对象
    Subject currentUser = SecurityUtils.getSubject();
    //获取session(shiro的session)
    Session session = currentUser.getSession();
    session.setAttribute("name","wangwu");

4.1.3 获取数据

可以在Service实现类中获取到数据

    @Override
    public User findUserByAccount(String account) {
        //从shiro中的session获取数据
        System.out.println(SecurityUtils.getSubject().getSession().getAttribute("name"));
        return userMapper.findUserByAccount(account);
    }

4.2 Session管理器

4.2.1 配置Session管理器

shiro配置类中:

    //7. shiro session的管理
    //7.1 配置session管理器
    @Bean
    public DefaultWebSessionManager sessionManager() {
        DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();
        // 去掉shiro登录时url里的JSESSIONID
        sessionManager.setSessionIdUrlRewritingEnabled(false);
        // 过期时间:单位 毫秒
        sessionManager.setGlobalSessionTimeout(30*60*1000);
        // 删除已经过期的session
        sessionManager.setDeleteInvalidSessions(true);
        return sessionManager;
    }

4.2.2 向SecurityManager注入Session管理器

shiro配置类中:

    //2.安全管理器(核心)
    @Bean   //参数将相当于  <property ref = "realm">
    public SecurityManager securityManager(CustomRealm realm,DefaultWebSessionManager sessionManager){
        DefaultWebSecurityManager manager = new DefaultWebSecurityManager();
        //设置realm
        manager.setRealm(realm);
        //7.2 注入session的管理器
        manager.setSessionManager(sessionManager);
        //
        return manager;
    }

五、Shiro标签 - 基于thymeleaf

5.1 pom.xml中导入依赖

      <!-- thymeleaf -->
      <dependency>
          <groupId>org.springframework.boot</groupId>
          <artifactId>spring-boot-starter-thymeleaf</artifactId>
      </dependency>
      <!-- shiro标签 -->
      <dependency>
          <groupId>com.github.theborakompanioni</groupId>
          <artifactId>thymeleaf-extras-shiro</artifactId>
          <version>2.0.0</version>
      </dependency>

5.2 shiro配置类

//8.配置shiro标签的方言
@Bean
public ShiroDialect dialect(){
    return new ShiroDialect();
}

5.3 修改main.html

<!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>
    <script src="/js/jquery.min.js"></script>
    <script>
        $(function () {
            $("#logout").click(function () {
                $.ajax({
                    url:"/user/logout",
                    success:function (res) {//json
                        console.log(res);
                        window.location.href = "/login.html";
                    }
                });
            });
        })
    </script>
</head>
<body>
<p>用户的后台管理页面</p>
    <!--
        hasRole             具有该角色,才能够看到
        hasAnyRole          具有其中任何一个角色都可以看到,就是or,多个角色用逗号隔开
        hasAllRole          必须具备所有角色才可以看到,就是and
        lacksRole           没有该角色可以看到
        hasPermission       具有某个权限才能看到
        hasAllPermissions   必须具备所有权限才能看到
        lacksPermissions    没有该权限可以看到
        authenticated       认证过才可以看到
        notAuthenticated    没有认证时能看到
        guest               只有游客能看到

        principal           获取到当前用户的账号
     -->
    <a href="/admin.html" shiro:hasRole="superAdmin">教师管理</a><br>
    <a href="/teacher.html" shiro:hasAllRoles="admin,teacher">成绩管理</a><br>
    <a href="/student.html" shiro:hasAllRoles="admin,teacher,student">分数查询</a><br>
    <a href="/adminAndTeacher.html" shiro:hasAllRoles="admin,teacher">密码管理</a><br>
    <a href="/adminOrTeacher.html" shiro:lacksRole="admin">信息查询</a><br>
    <button id="logout">注销</button>
</body>
</html>

将main.html移动到templates目录下,该目录专门用来存放使用了模板引擎的页面
而且直接访问main.html不会生效,因为采用了模板引擎的页面必须通过后台代码处理
**

5.4 修改JumpController

//去主页面
@RequestMapping("/main")
public String toMain(){
    System.out.println("通过后台跳转到首页");
    return "/main.html";
}

5.5 修改login.html

success:function (res) {
    console.log(res);
    //请求后台跳转到首页
    if(res=="success"){
        //跳转页面
        window.location.href="/jump/main";
    }
}

六、Remember Me

在shiro有一个过滤器叫做user,用user修饰的url可以在第一次然后没有注销的情况下,下一次可以直接访问

在static下创建rm.html

<html>
  <body>
      remember me
  </body>
</html>

修改login.html,添加记住我复选框

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <script src="/js/jquery.min.js"></script>
    <script>
        $(function () {
            $("#login").click(function () {
                let account = $("#account").val();
                let pwd = $("#pwd").val();
                let rm = $("rm").prop("checked");
                $.ajax({
                    url:"user/login",
                    type:"POST",
                    data:{
                        account:account,
                        pwd:pwd,
                        rm:rm
                    },
                    success:function (res) {
                        console.log(res);
                        //请求后台跳转到首页
                        if(res=="success"){
                            window.location.href="/jump/main";
                        }
                    }
                });
            });
        });
    </script>
</head>
<body>
    <div>
        <input type="text" id="account"><br>
        <input type="password" id="pwd"><br>
        <input type="checkbox" id="rm">记住我<br>
        <button id="login">登录</button>
    </div>
</body>
</html>

修改User,添加记住我属性

@Data
@Accessors(chain = true)
public class User {
    private int id;
    private String account;
    private String pwd;
    private List<Role> roles;
    private boolean rm; //记住我选项
}

修改UserController登录方法,判断用户是否勾选记住我

@RestController //
@RequestMapping("/user")
public class UserController {
    private static final Logger logger = LoggerFactory.getLogger(UserController.class);
    @RequestMapping("/login")
    public String login(User user){
        //1.获取到“当前用户”对象
        Subject currentUser = SecurityUtils.getSubject();
        //获取session
        Session session = currentUser.getSession();
        session.setAttribute("name","lisi");
        //2.判断用户是否认证过
        if (!currentUser.isAuthenticated()){
            //没有认证过   token  令牌
            String pwd = new SimpleHash("MD5",user.getPwd(),user.getAccount(),100).toString();
            UsernamePasswordToken token =
                    new UsernamePasswordToken(user.getAccount(),pwd);
            //判断用户是否勾选记住我
            if (user.isRm()){
                token.setRememberMe(true);//使用记住我功能
            }
            try {
                currentUser.login(token);
                //
                logger.info("认证成功");
                return "success";
            }catch(Exception e)
                return "密码错误";
            }
        }
        return "success";
    }
}

在shiro的过滤器中使用user过滤器配置url

//使用记住我过滤器
map.put("/rm.html","user");

运行项目测试
测试步骤

先正常登陆
关闭浏览器
访问main页面
登陆勾选记住我
访问rm.html
关闭浏览器
访问main.html    不能访问
访问rm.html      可以访问

注意:全程不要注销