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