spring boot 使用Shiro进行用户认证教程

1.依赖

 <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-spring</artifactId>
            <version>1.4.0</version>
        </dependency>

2.创建测试数据

1.创建数据库用户数据
在这里插入图片描述
2.创建UserMapper 获取用户数据

@Mapper
public interface UserMapper {
     List<Userinfo> findAll();
}

UserMapper.xml

<?xml version="1.0" encoding ="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper3.0//EN "
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.demo2.mapper.UserMapper">
    <select id="findAll" resultType="com.example.demo2.beans.Userinfo">
         select * from userinfo;
    </select>
</mapper>

3.创建ShiroConfig,添加相关设置

这里面比如是否需要加密密码(认证传入Shiro的是明文,但是数据库保存的可能是加密过的,所以需要相关的加密配置,Shiro根据这个配置对认证时传入的密码进行加密,然后进行匹配)

package com.example.demo2.config;

import com.example.demo2.app.UserRealm;
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.LinkedHashMap;
import java.util.Map;

@Configuration
public class ShiroConfig {
    @Bean
    public HashedCredentialsMatcher hashedCredentialsMatcher() {
        HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
        //md5加密1次
        hashedCredentialsMatcher.setHashAlgorithmName("md5");
        hashedCredentialsMatcher.setHashIterations(1);
        hashedCredentialsMatcher.setHashSalted(true);
        return hashedCredentialsMatcher;
    }


    //自定义realm
    @Bean
    public UserRealm userRealm() {
        UserRealm userRealm = new UserRealm();
        userRealm.setCredentialsMatcher(hashedCredentialsMatcher());
        return userRealm;
    }
    /**
     * 安全管理器
     * 注:使用shiro-spring-boot-starter 1.4时,返回类型是SecurityManager会报错,直接引用shiro-spring则不报错
     *
     * @return
     */
    @Bean
    public DefaultWebSecurityManager securityManager() {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        securityManager.setRealm(userRealm());
        return securityManager;
    }


    /**
     * 设置过滤规则
     * @return
     */
    @Bean
    public ShiroFilterFactoryBean shiroFilter(SecurityManager manager) {
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        shiroFilterFactoryBean.setLoginUrl("/login");
        shiroFilterFactoryBean.setSecurityManager(manager);
        shiroFilterFactoryBean.setSuccessUrl("/");
        shiroFilterFactoryBean.setUnauthorizedUrl("/unauth");

        //注意此处使用的是LinkedHashMap,是有顺序的,shiro会按从上到下的顺序匹配验证,匹配了就不再继续验证
        //所以上面的url要苛刻,宽松的url要放在下面,尤其是"/**"要放到最下面,如果放前面的话其后的验证规则就没作用了。
        Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
        filterChainDefinitionMap.put("/static/**", "anon");
        filterChainDefinitionMap.put("/login", "anon");
        filterChainDefinitionMap.put("/captcha.jpg", "anon");
        filterChainDefinitionMap.put("/favicon.ico", "anon");
        filterChainDefinitionMap.put("/**", "authc");
        shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
        return shiroFilterFactoryBean;
    }

}

4.创建UserRealm,用于身份权限划分与认证匹配

package com.example.demo2.app;

import com.example.demo2.beans.Userinfo;
import com.example.demo2.mapper.UserMapper;
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.subject.PrincipalCollection;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.util.DigestUtils;

import java.util.ArrayList;
import java.util.List;

public class UserRealm extends AuthorizingRealm {
    @Autowired
    UserMapper userMapper;

    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
        //添加授权认证用户,一般是添加用户信息
        //一般在这里进行用户权限区分,把那些有权限的用户加入进去
        info.addStringPermissions(getUserNames());
        return info;
    }



    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
      //获取认证信息
        UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken;
        String userName = token.getUsername();
        //一般来说,获取到要验证的用户id,然后我们获取数据库的密码,然后把账号密码传入进去就行了
        //验证是有Shrio自动帮我们匹配
        Userinfo userinfo = getUserByUserId(userName);
        if(userinfo!=null){
            //获取MD5加密后的密码,一般来说这个应该是存在数据库的,不是明文密码
            //为了测试方便,生成MD5密码传入,在ShrioConfig里设置MD5加密一次,看上面ShiroConfig 
            //到时候验证时传入明文密码,Shrio自动帮我们加密后再匹配
            //就是说这个密码是数据库保存的密码,一般来说,保存是啥就传入啥
            String pswInSecCode =  getMD5(userinfo.getPassword());
            return new SimpleAuthenticationInfo(userinfo.getUserName(), pswInSecCode, getName());

        }
        //找不到,说明用户不存在,返回空
        return null;
    }

    public  static String getMD5(String str) {
        String base = str+"";
        String md5 = DigestUtils.md5DigestAsHex(base.getBytes());
        return md5;
    }


    private List<String> getUserNames(){
        List<Userinfo> userinfos = userMapper.findAll();
        List<String> userNames = new ArrayList<>();
        if (userinfos != null && userinfos.size() > 0) {
            for (Userinfo userinfo : userinfos) {
                userNames.add(userinfo.getUserName());
            }
        }
        return userNames;
    }

    private Userinfo getUserByUserId(String userId){
        List<Userinfo> userinfos = userMapper.findAll();
        if (userinfos != null && userinfos.size() > 0) {
            for (Userinfo userinfo : userinfos) {
               if(userinfo.getUserName().equals(userId)){
                   return userinfo;
               }
            }
        }
        return null;
    }
}

5.添加LoginController,用于登录请求

package com.example.demo2.controller;

import com.example.demo2.config.ShiroConfig;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.DisabledAccountException;
import org.apache.shiro.authc.IncorrectCredentialsException;
import org.apache.shiro.authc.UnknownAccountException;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.subject.Subject;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller
public class LoginController {
    /**
     * get请求,登录页面跳转
     *
     * @return
     */
    @GetMapping("/login")
    public String login() {
        //如果已经认证通过,直接跳转到首页
        if (SecurityUtils.getSubject().isAuthenticated()) {
            return "redirect:/index";
        }
        return "login";
    }

    /**
     * post表单提交,登录
     *
     * @param username
     * @param password
     * @param model
     * @return
     */
    @PostMapping("/login")
    public Object login(String username, String password, Model model) {
        Subject user = SecurityUtils.getSubject();
        UsernamePasswordToken token = new UsernamePasswordToken(username,password);
        System.out.println("username:" + username + ",psw:" + password);
        try {
            //shiro帮我们匹配密码什么的,我们只需要把东西传给它,它会根据我们在UserRealm里认证方法设置的来验证
            user.login(token);
            return "redirect:index";
        } catch (UnknownAccountException e) {
            //账号不存在和下面密码错误一般都合并为一个账号或密码错误,这样可以增加暴力破解难度
            model.addAttribute("message", "账号不存在!");
        } catch (DisabledAccountException e) {
            model.addAttribute("message", "账号未启用!");
        } catch (IncorrectCredentialsException e) {
            model.addAttribute("message", "密码错误!");
        } catch (Throwable e) {
            model.addAttribute("message", "未知错误!");
        }
        return "login";
    }



    /**
     * 退出
     *
     * @return
     */
    @RequestMapping("/logout")
    public String logout() {
        SecurityUtils.getSubject().logout();
        return "login";
    }


}

6.添加IndexController用于登录成功后跳转请求

package com.example.demo2.controller;

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

@Controller
public class IndexController {
    /**
     * 首页,并将登录用户的全名返回前台
     * @param model
     * @return
     */
    @RequestMapping(value = {"/", "/index"})
    public String index(Model model) {
        String sysUser = (String) SecurityUtils.getSubject().getPrincipal();
        model.addAttribute("userName", sysUser);
        return "index";
    }
}

7.添加jsp,用于前端测试

依赖:

   <dependency>
            <groupId>org.apache.tomcat.embed</groupId>
            <artifactId>tomcat-embed-jasper</artifactId>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>javax.servlet</groupId>
            <artifactId>jstl</artifactId>
        </dependency>

添加jsp:
在这里插入图片描述
login.jsp用户登录:

<%@ page contentType="text/html;charset=UTF-8" pageEncoding="UTF-8" %>
<!DOCTYPE html>
<html>
<head>
    <meta http-equiv="Content-type" content="text/html; charset=UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1"/>
    <title>登录</title>
</head>
<body>
<h1>${message}</h1>
<form method="post" action="/login">
    用户名:<input name="username"><br>
    密码:<input name="password"><br>
    <input type="submit" value="登录">
</form>
</body>
</html>

index.jsp用于登录成功:

<%@ page contentType="text/html;charset=UTF-8" pageEncoding="UTF-8" %>
<!DOCTYPE html>
<html>
<head>
    <meta http-equiv="Content-type" content="text/html; charset=UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1"/>
    <title>首页</title>
</head>
<body>
hello ${userName}
</body>
</html>

8.运行测试

输入密码错误提示:
在这里插入图片描述
输入密码成功后跳转:
在这里插入图片描述

9.session管理

一般默认都支持session,按照上面的教程,没有登录或者session过期后访问任何接口与任何页面都会返回登录界面。
我们可以自定义session配置,在上面的ShiroConfig类里,添加下面:

 /**
     * 安全管理器
     * 注:使用shiro-spring-boot-starter 1.4时,返回类型是SecurityManager会报错,直接引用shiro-spring则不报错
     *
     * @return
     */
    @Bean
    public DefaultWebSecurityManager securityManager() {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        securityManager.setRealm(userRealm());
        securityManager.setSessionManager(sessionManager());
        return securityManager;
    }
    
//使用默认的MemorySessionDAO
    @Bean
    public SessionDAO sessionDAO() {
        return new MemorySessionDAO();
    }

    /**
     * shiro session的管理
     */
    @Bean
    public DefaultWebSessionManager sessionManager() {  //配置默认的sesssion管理器
        DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();
        //设置session过期为60秒,默认30分钟
        sessionManager.setGlobalSessionTimeout(60 * 1000);
        sessionManager.setSessionDAO(sessionDAO());
        Collection<SessionListener> listeners = new ArrayList<>();
        listeners.add(new BDSessionListener());
        sessionManager.setSessionListeners(listeners);
        return sessionManager;
    }

    //Session监听
    private class BDSessionListener implements SessionListener{
        @Override
        public void onStart(Session session) {

        }

        @Override
        public void onStop(Session session) {

        }

        @Override
        public void onExpiration(Session session) {

        }
    }

浏览器登录成功后。60秒内,关闭页面再进入,不需要重新登录:
在这里插入图片描述
不操作60秒后,需要重新登录。

取消对接口的限制

Shiro可以编译过滤拦截规则,具体在ShiroConfig里面。

   /**
     * 设置过滤规则
     * @return
     */
    @Bean
    public ShiroFilterFactoryBean shiroFilter(SecurityManager manager) {
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        shiroFilterFactoryBean.setLoginUrl("/notLogin");
        shiroFilterFactoryBean.setLoginUrl("/login");
        shiroFilterFactoryBean.setSecurityManager(manager);
        shiroFilterFactoryBean.setSuccessUrl("/");     
        shiroFilterFactoryBean.setUnauthorizedUrl("/notRole");

        //注意此处使用的是LinkedHashMap,是有顺序的,shiro会按从上到下的顺序匹配验证,匹配了就不再继续验证
        //所以上面的url要苛刻,宽松的url要放在下面,尤其是"/**"要放到最下面,如果放前面的话其后的验证规则就没作用了。
        Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
        filterChainDefinitionMap.put("/static/**", "anon");
        filterChainDefinitionMap.put("/login", "anon");
        filterChainDefinitionMap.put("/captcha.jpg", "anon");
        filterChainDefinitionMap.put("/favicon.ico", "anon");
        filterChainDefinitionMap.put("/**", "authc");
        shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
        return shiroFilterFactoryBean;
    }

按照这个规则,所有请求接口都会拦截,就是任何请求,没有登录过都会返回登录页面。
可以按照下面的修改,只拦截指定页面:

/**
     * 设置过滤规则
     * @return
     */
    @Bean
    public ShiroFilterFactoryBean shiroFilter(SecurityManager manager) {
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        shiroFilterFactoryBean.setLoginUrl("/notLogin");
        shiroFilterFactoryBean.setLoginUrl("/login");
        shiroFilterFactoryBean.setSecurityManager(manager);
        shiroFilterFactoryBean.setSuccessUrl("/");     
        shiroFilterFactoryBean.setUnauthorizedUrl("/notRole");

        //注意此处使用的是LinkedHashMap,是有顺序的,shiro会按从上到下的顺序匹配验证,匹配了就不再继续验证
        //所以上面的url要苛刻,宽松的url要放在下面,尤其是"/**"要放到最下面,如果放前面的话其后的验证规则就没作用了。
        Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
        filterChainDefinitionMap.put("/static/**", "anon");
        filterChainDefinitionMap.put("/login", "anon");
        filterChainDefinitionMap.put("/captcha.jpg", "anon");
        filterChainDefinitionMap.put("/favicon.ico", "anon");
        //authc是需要认证的,anon是不需要拦截认证
       // filterChainDefinitionMap.put("/**", "authc");
        //我们这里设置index才需要拦截认证,其他接口都不拦截即可
        filterChainDefinitionMap.put("/index", "authc");

        shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
        return shiroFilterFactoryBean;
    }

具体一些更多相关,可以参下这篇文章https://www.cnblogs.com/qsymg/p/9836122.html

相关参考:
本文参考借鉴于https://blog.csdn.net/gnail_oug/article/details/80662553,感谢。

10.前后端分离的情况下处理

前后端分离时,登录地址可设置为绝对路径:

  @Bean
    public ShiroFilterFactoryBean shiroFilter(SecurityManager manager) {
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        shiroFilterFactoryBean.setLoginUrl("http://localhost:8081/login");
        shiroFilterFactoryBean.setSecurityManager(manager);
        shiroFilterFactoryBean.setSuccessUrl("http://localhost:8081/manager");
        shiroFilterFactoryBean.setUnauthorizedUrl("http://localhost:8081/login");

        //注意此处使用的是LinkedHashMap,是有顺序的,shiro会按从上到下的顺序匹配验证,匹配了就不再继续验证
        //所以上面的url要苛刻,宽松的url要放在下面,尤其是"/**"要放到最下面,如果放前面的话其后的验证规则就没作用了。
        //注意此处使用的是LinkedHashMap,是有顺序的,shiro会按从上到下的顺序匹配验证,匹配了就不再继续验证
        //所以上面的url要苛刻,宽松的url要放在下面,尤其是"/**"要放到最下面,如果放前面的话其后的验证规则就没作用了。
        Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
    
        filterChainDefinitionMap.put("/loginCheck", "authc");
        filterChainDefinitionMap.put("/**", "anon");        shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
        return shiroFilterFactoryBean;
    }

有这么一种情况,并且绝大部分接口允许不验证访问,比如管理端的接口都是需要登录验证的,那只需要过滤管理端用到的接口即可,如上面的loginCheck。测试如下:

没有登录时,调用loginCheck,无权限
在这里插入图片描述
登录成功后再调用:
在这里插入图片描述
退出登录后再调用:
在这里插入图片描述

11.接口请求权限测试

在filter中设置了请求是无权限的url

  @Bean
    public ShiroFilterFactoryBean shiroFilter(SecurityManager manager) {
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        shiroFilterFactoryBean.setLoginUrl("/notLogin");
        shiroFilterFactoryBean.setSecurityManager(manager);
        shiroFilterFactoryBean.setSuccessUrl("http://localhost:8081/manager");
        shiroFilterFactoryBean.setUnauthorizedUrl("/notRole");

在controller中添加这个url:

  @RequestMapping(value = "/notLogin", method = RequestMethod.GET)
    public ApiFinalResult notLogin() {
        return new ApiFinalResult(AppConstant.ApiErrorType.ELSE,"尚未登录!",null);
    }



    @RequestMapping(value = "/notRole", method = RequestMethod.GET)
    public ApiFinalResult notRole() {
        return new ApiFinalResult(AppConstant.ApiErrorType.ELSE,"您无权限!",null);
    }

那个如果服务需要登录才能使用的接口,就会返回如下:
在这里插入图片描述

12.前后端情况下,vue前端登录成功也是提示尚未登录

应该就是没带上session的问题。Axios添加以下设置

 Axios.defaults.withCredentials = true
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值