shiro介绍
Shiro是一个强大易用的Java安全框架,提供了认证、授权、加密和会话管理等功能。
- 引入shiro依赖
<!-- shiro-->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.3.2</version>
</dependency>
- 创建shiroConfig类(shiro的基本配置),其中包含过滤器,开启注解配置权限以及加密方法(注册时调用)
package com.example.demo.config;
import com.example.demo.service.CustomRealm;
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.apache.shiro.crypto.hash.SimpleHash;
import org.apache.shiro.spring.LifecycleBeanPostProcessor;
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.util.ByteSource;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.apache.shiro.mgt.SecurityManager;
import org.springframework.context.annotation.DependsOn;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* @author ligaode
* @date 2021/11/5 9:47
* ShiroConfig
*/
@Configuration
public class ShiroConfig {
@Bean(name = "shiroFilter")
public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
shiroFilterFactoryBean.setSecurityManager(securityManager);
shiroFilterFactoryBean.setLoginUrl("/api/login");
shiroFilterFactoryBean.setUnauthorizedUrl("/api/notRole");
Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
// <!-- authc:所有url都必须认证通过才可以访问; anon:所有url都都可以匿名访问-->
//放行Swagger2页面,需要放行这些
filterChainDefinitionMap.put("/swagger-ui.html","anon");
filterChainDefinitionMap.put("/swagger/**","anon");
filterChainDefinitionMap.put("/webjars/**", "anon");
filterChainDefinitionMap.put("/swagger-resources/**","anon");
filterChainDefinitionMap.put("/v2/**","anon");
filterChainDefinitionMap.put("/static/**", "anon");
// 放行登录注册接口
filterChainDefinitionMap.put("/api/getCode", "anon");//放行获取验证码
filterChainDefinitionMap.put("/api/login", "anon");//放行登录
filterChainDefinitionMap.put("/api/register", "anon");//放行注册
// filterChainDefinitionMap.put("/api/**", "anon");
// filterChainDefinitionMap.put("/admin/**", "authc");
// filterChainDefinitionMap.put("/user/**", "authc");
//主要这行代码必须放在所有权限设置的最后,不然会导致所有 url 都被拦截 剩余的都需要认证
filterChainDefinitionMap.put("/**", "authc");
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
return shiroFilterFactoryBean;
}
@Bean
public SecurityManager securityManager() {
DefaultWebSecurityManager defaultSecurityManager = new DefaultWebSecurityManager();
defaultSecurityManager.setRealm(customRealm());
return defaultSecurityManager;
}
@Bean
public CustomRealm customRealm() {
CustomRealm customRealm = new CustomRealm();
// 告诉realm,使用credentialsMatcher加密算法类来验证密文
customRealm.setCredentialsMatcher(hashedCredentialsMatcher());
customRealm.setCachingEnabled(false);
return customRealm;
}
// 利用注解配置权限:
// 其实,我们完全可以不用注解的形式去配置权限,因为在之前已经加过了:DefaultFilter类中有perms(类似于perms[user:add])这种形式的。但是试想一下
// ,这种控制的粒度可能会很细,具体到某一个类中的方法,那么如果是配置文件配,是不是每个方法都要加一个perms?但是注解就不一样了,直接写在方法上面,简单快捷。
// 很简单,主需要在config类中加入如下代码,就能开启注解:
@Bean
public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
return new LifecycleBeanPostProcessor();
}
/**
* *
* 开启Shiro的注解(如@RequiresRoles,@RequiresPermissions),需借助SpringAOP扫描使用Shiro注解的类,并在必要时进行安全逻辑验证
* *
* 配置以下两个bean(DefaultAdvisorAutoProxyCreator(可选)和AuthorizationAttributeSourceAdvisor)即可实现此功能
* * @return
*/
@Bean
@DependsOn({"lifecycleBeanPostProcessor"})
public DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator() {
DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
advisorAutoProxyCreator.setProxyTargetClass(true);
return advisorAutoProxyCreator;
}
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor() {
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
authorizationAttributeSourceAdvisor.setSecurityManager(securityManager());
return authorizationAttributeSourceAdvisor;
}
//加密
@Bean(name = "credentialsMatcher")
public HashedCredentialsMatcher hashedCredentialsMatcher() {
HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
// 散列算法:这里使用MD5算法;
hashedCredentialsMatcher.setHashAlgorithmName("md5");
// 散列的次数,比如散列两次,相当于 md5(md5(""));
hashedCredentialsMatcher.setHashIterations(2);
// storedCredentialsHexEncoded默认是true,此时用的是密码加密用的是Hex编码;false时用Base64编码
hashedCredentialsMatcher.setStoredCredentialsHexEncoded(true);
return hashedCredentialsMatcher;
}
//在注册的时候调用此方法用来加密密码
public static String MD5Pwd(String phone, String pwd) {
// 加密算法MD5
// salt盐 username + salt
// 迭代次数
String md5Pwd = new SimpleHash("MD5", pwd,
ByteSource.Util.bytes(phone + "salt"), 2).toHex();
return md5Pwd;
}
}
Subject: 代表当前正在执行操作的用户,但Subject代表的可以是人,也可以是任何第三方系统帐号。当然每个subject实例都会被绑定到SercurityManger上。
SecurityManger:SecurityManager是Shiro核心,主要协调Shiro内部的各种安全组件,这个我们不需要太关注,只需要知道可以设置自定的Realm。
Realm:用户数据和Shiro数据交互的桥梁。比如需要用户身份认证、权限认证。都是需要通过Realm来读取数据。
- 创建customRealm类
customRealm配置:
package com.example.demo.service;
import com.example.demo.entity.User;
import com.example.demo.mapper.LoginMapper;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
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.apache.shiro.authc.*;
import org.apache.shiro.util.ByteSource;
import org.springframework.beans.factory.annotation.Autowired;
import java.util.HashSet;
import java.util.Set;
/**
* @author ligaode
* @date 2021/11/5 9:46
* Realm
*/
@Slf4j
public class CustomRealm extends AuthorizingRealm {
@Autowired
private LoginMapper loginMapper;
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
String username = (String) SecurityUtils.getSubject().getPrincipal();
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
Set<String> stringSet = new HashSet<>();
stringSet.add("user:admin");
stringSet.add("user:user");
info.setStringPermissions(stringSet);
return info;
}
//进行身份验证
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
System.out.println("-------身份认证方法--------");
String userPhone = (String) authenticationToken.getPrincipal();
// String userPwd = new String((char[]) authenticationToken.getCredentials());
//根据用户名从数据库获取密码
User user=loginMapper.login_shiro(userPhone);
String phone=user.getPhone();
String password = user.getPassword();
if (phone == null) {
throw new AccountException("用户名不正确");
}
// else if (!userPwd.equals(password )) {
//
// throw new AccountException("密码不正确");
// }
//交给AuthenticatingRealm使用CredentialsMatcher进行密码匹配
//注册的加密方式和设置的加密方式还有Realm中身份认证的方式都是要一模一样的。
return new SimpleAuthenticationInfo(phone, password,
ByteSource.Util.bytes(phone + "salt"), getName());
}
}
- 登陆接口
@GetMapping(value = "api/login")
@ResponseBody
public String login(
@RequestParam(value = "phone") String phone,
@RequestParam(value = "password") String password,
@RequestParam(value = "code") @ApiParam(value = "验证码") String code,
HttpServletRequest httpServletRequest)
{
//判断验证码是否正确
if (code!=null){
String Code= httpServletRequest.getSession().getAttribute("rightCode").toString();
if (!Code.equalsIgnoreCase(code)){
return "验证码错误";
}
}else {
return "请输入验证码";
}
// 从SecurityUtils里边创建一个 subject
Subject subject = SecurityUtils.getSubject();
// 在认证提交前准备 token(令牌)
UsernamePasswordToken token = new UsernamePasswordToken(phone, password);
// 执行认证登陆
try {
subject.login(token);
} catch (UnknownAccountException uae) {
return "未知账户";
} catch (IncorrectCredentialsException ice) {
return "密码不正确";
} catch (LockedAccountException lae) {
return "账户已锁定";
} catch (ExcessiveAttemptsException eae) {
return "用户名或密码错误次数过多";
} catch (AuthenticationException ae) {
return "用户名或密码不正确!";
}
if (subject.isAuthenticated()) {
return "登录成功";
} else {
token.clear();
return "登录失败";
}
}
- 权限验证测试类
package com.example.demo.controller;
import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.springframework.stereotype.Controller;
import org.springframework.stereotype.Repository;
import org.springframework.web.bind.annotation.*;
/**
* @author ligaode
* @date 2021/11/5 10:26
* UserController
*/
@RestController
public class UserController {
//注解配置权限
@RequiresPermissions("user:user")
@CrossOrigin
// @ResponseBody
@GetMapping("/show")
public String showUser() {
return "这是学生信息";
}
}
- 退出登录接口,清楚shiro的session
//退出登录
@CrossOrigin
@GetMapping(value = "api/logout")
public String logout( HttpServletRequest httpServletRequest){
Subject subject = SecurityUtils.getSubject();
if(subject.isAuthenticated()) {
subject.logout();
}
return "退出登录";
}
- 新建NoPermissionException类,用于解决当没有权限时,系统会报错,而没有跳转到对应的没有权限的页面,也就是setUnauthorizedUrl这个方法没起作用
package com.example.demo.service;
import org.apache.shiro.authz.AuthorizationException;
import org.apache.shiro.authz.UnauthorizedException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
/**
* @author ligaode
* @date 2021/11/5 13:26
* NoPermissionException
*/
@ControllerAdvice
public class NoPermissionException {
@ResponseBody
@ExceptionHandler(UnauthorizedException.class)
public String handleShiroException(Exception ex) {
return "无权限";
}
@ResponseBody
@ExceptionHandler(AuthorizationException.class)
public String AuthorizationException(Exception ex) {
return "权限认证失败";
}
}
附带当时学习shiro时的参考博客:https://blog.csdn.net/bicheng4769/article/details/86668209