1.shiro是什么?
Apache Shiro是一个强大且易用的Java安全框架,执行身份验证、授权、密码和会话管理。使用Shiro的易于理解的API,您可以快速、轻松地获得任何应用程序,从最小的移动应用程序到最大的网络和企业应用程序。
官方架构图如下:
2.主要功能
shiro主要有三大功能模块:
1. Subject:主体,一般指用户。
2. SecurityManager:安全管理器,管理所有Subject,可以配合内部安全组件。(类似于SpringMVC中的DispatcherServlet)
3. Realms:用于进行权限信息的验证,一般需要自己实现。
3.细分功能
1. Authentication:身份认证/登录(账号密码验证)。
2. Authorization:授权,即角色或者权限验证。
3. Session Manager:会话管理,用户登录后的session相关管理。
4. Cryptography:加密,密码加密等。
5. Web Support:Web支持,集成Web环境。
6. Caching:缓存,用户信息、角色、权限等缓存到如redis等缓存中。
7. Concurrency:多线程并发验证,在一个线程中开启另一个线程,可以把权限自动传播过去。
8. Testing:测试支持;
9. Run As:允许一个用户假装为另一个用户(如果他们允许)的身份进行访问。
10. Remember Me:记住我,登录后,下次再来的话不用登录了。
接下来就上代码结构
本人在做shiro整合的同时又加了aop 以及自定义注解 这些都可以不看主要是shiro整合
pom.xml
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.8.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.4.0</version>
</dependency>
<!-- msyql-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<!-- lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!-- redis 缓存数据库-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- commons-lang3 工具包-->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.3.2</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-all</artifactId>
<version>1.4.2</version>
<exclusions>
<exclusion>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-quartz</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.17</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
Account实体类(用户对应实体类)
package com.sykj.bean;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
@Data
@NoArgsConstructor
@AllArgsConstructor
@TableName("account")
public class Account implements Serializable {
@TableId(type = IdType.AUTO)
private Integer accid;
@TableField
private String accname;
@TableField
private String accpass;
@TableField
private String accstate;
}
Permission 实体类(权限对应实体类)
package com.sykj.bean;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
@Data
@NoArgsConstructor
@AllArgsConstructor
@TableName("permission")
public class Permission implements Serializable {
@TableId
private Integer pid;
@TableField
private String pname;
@TableField
private String code;
}
Role实体类(角色应实体类)
package com.sykj.bean;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
@Data
@NoArgsConstructor
@AllArgsConstructor
@TableName("role")
public class Role implements Serializable {
@TableId
private Integer rid;
@TableField
private String rname;
@TableField
private String remark;
}
ShiroConfig配置类
package com.sykj.config;
import com.sykj.realm.MyRealm;
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* shiro配置类
*/
@Configuration
public class ShiroConfig {
//1. realm对象注入
@Bean
public MyRealm myRealm(){
MyRealm myRealm=new MyRealm();
//设置加密方式
myRealm.setCredentialsMatcher(hashedCredentialsMatcher());
// //配置自定义密码比较器
// myRealm.setCredentialsMatcher(retryLimitHashedCredentialsMatcher());
return myRealm;
}
/*
* @describe 自定义凭证匹配器 配置密码比较器
* (由于我们的密码校验交给Shiro的SimpleAuthenticationInfo进行处理了所以我们需要修改下doGetAuthenticationInfo中的代码)
* 可以扩展凭证匹配器,实现输入密码错误次数后锁定等功能
* @return org.apache.shiro.authc.credential.HashedCredentialsMatcher
*/
@Bean(name = "credentialsMatcher")
public CustomerMatcher hashedCredentialsMatcher() {
CustomerMatcher hashedCredentialsMatcher = new CustomerMatcher();
hashedCredentialsMatcher.setHashAlgorithmName("MD5");//散列算法:这里使用MD5算法;
hashedCredentialsMatcher.setHashIterations(1024);//散列的次数,比如散列两次,相当于 md5(md5(""));
//storedCredentialsHexEncoded默认是true,此时用的是密码加密用的是Hex编码;false时用Base64编码
// hashedCredentialsMatcher.setStoredCredentialsHexEncoded(true);
return hashedCredentialsMatcher;
}
//2. securityManager安全管理器注入
//2.安全管理器
@Bean
public SecurityManager securityManager(HashedCredentialsMatcher hashedCredentialsMatcher){
DefaultWebSecurityManager securityManager=new DefaultWebSecurityManager();
securityManager.setRealm(myRealm());
// //配置 ehcache缓存管理器 参考博客:
// securityManager.setCacheManager(ehCacheManager());
return securityManager;
}
//3. shiro过滤器配置
@Bean
public ShiroFilterFactoryBean factory(SecurityManager securityManager) {
ShiroFilterFactoryBean shiroFilterFactoryBean=new ShiroFilterFactoryBean();
shiroFilterFactoryBean.setSecurityManager(securityManager);
//登录认证不通过跳转的url
shiroFilterFactoryBean.setLoginUrl("/user/loginError");
//功能权限不足跳转个URL
shiroFilterFactoryBean.setUnauthorizedUrl("/user/unAuthError");
Map map=new LinkedHashMap();
map.put("/user/login","anon");//放行,允许不登录就直接访问,不拦截
map.put("/user/loginError","anon");
map.put("/user/unAuthError","anon");
map.put("/**","authc");//登录拦截器
shiroFilterFactoryBean.setFilterChainDefinitionMap(map);
return shiroFilterFactoryBean;
}
//4. shiro注解开启
/**
* 开启aop注解支持
* 即在controller中使用 @RequiresPermissions("user/userList")
*/
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager){
AuthorizationAttributeSourceAdvisor attributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
//设置安全管理器
attributeSourceAdvisor.setSecurityManager(securityManager);
return attributeSourceAdvisor;
}
@Bean
@ConditionalOnMissingBean
public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
DefaultAdvisorAutoProxyCreator defaultAAP = new DefaultAdvisorAutoProxyCreator();
defaultAAP.setProxyTargetClass(true);
return defaultAAP;
}
}
MyRealm (登录认证、权限鉴权的验证类 )
此类为自定义类
package com.sykj.realm;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.sykj.bean.Account;
import com.sykj.bean.Permission;
import com.sykj.bean.Role;
import com.sykj.service.AccountService;
import com.sykj.service.PermissionService;
import com.sykj.service.RoleService;
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.apache.shiro.util.ByteSource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
public class MyRealm extends AuthorizingRealm {
@Autowired
private AccountService accountService;
@Autowired
private RoleService roleService;
@Autowired
private PermissionService permissionService;
@Autowired
private RedisTemplate redisTemplate;
/**
* 巨坑 建议重写
* @param token
* @return
*/
@Override
public boolean supports(AuthenticationToken token) {
return token instanceof UsernamePasswordToken;
}
/**
* 权限认证
* @param principalCollection
* @return
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
System.out.println("开始权限认证---------------------");
// Account account= (Account) principalCollection.getPrimaryPrincipal();//取出用户登录信息
Account account= (Account) principalCollection.getPrimaryPrincipal();//登陆成功保存的用户信息
System.out.println(account);
//调用服务层方法获取该用户的所有角色信息
List<Role> roles=roleService.queryRoles(account.getAccid());
//保存角色信息 用set集合 保证去重复
Set<String> roleSet=new HashSet<>();
//保存资源信息
Set<String> permssionSet=new HashSet<>();
//判断redis上是否存在key 存放角色和权限信息
if(redisTemplate.hasKey("account:roleAndPermission:"+account.getAccid())){
System.out.println("从redis中取出数据----------------->>>>>>>>>>>>>>>>>>>>>.");
//取出角色信息
roleSet= (Set<String>) redisTemplate.opsForHash().get("account:roleAndPermission:"+account.getAccid(),"roles");
//取出权限信息
permssionSet= (Set<String>) redisTemplate.opsForHash().get("account:roleAndPermission:"+account.getAccid(),"permission");
}else{
System.out.println("向redis中存放数据-----------。》》》》》》》》》》》》》");
for(Role role:roles){
roleSet.add(role.getRname());
List<Permission> permissions=permissionService.querypermiss(role.getRid());
for (Permission permission:permissions){
permssionSet.add(permission.getCode());
}
}
//向redis中存放数据 角色 和权限信息
redisTemplate.opsForHash().put("account:roleAndPermission:"+account.getAccid(),"roles",roleSet);
redisTemplate.opsForHash().put("account:roleAndPermission:"+account.getAccid(),"permission",permssionSet);
}
SimpleAuthorizationInfo info=new SimpleAuthorizationInfo();//保存用户的角色和权限信息
info.addRoles(roleSet);
info.addStringPermissions(permssionSet);
return info;
}
/**
*
* 登录认证
* @param authenticationToken
* @return
* @throws AuthenticationException
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
System.out.println("-------------登录认证");
UsernamePasswordToken token= (UsernamePasswordToken) authenticationToken;
String accname=token.getUsername();
QueryWrapper wrapper=new QueryWrapper();
wrapper.eq("accname",accname);
Account account= accountService.getOne(wrapper);
if (account==null){
throw new UnknownAccountException();
}
if (account.getAccstate().equals("0")){
throw new LockedAccountException("账号被锁定,请及时联系管理员。");
}
//加盐并编码
ByteSource salt = ByteSource.Util.bytes("sykj");
System.out.println("=================================");
System.out.println(account.getAccpass());
SimpleAuthenticationInfo info=new SimpleAuthenticationInfo(account,account.getAccpass(),salt,getName());
return info;
}
}
Aes 加密解密工具类
package com.sykj.util;
import org.apache.commons.codec.binary.Base64;
import javax.crypto.Cipher;
import javax.crypto.spec.SecretKeySpec;
/*****************************************************
* AES加密解密工具
****************************************************/
public class AesUtil {
private static final String ALGORITHM = "AES/ECB/PKCS5Padding"; //"算法/模式/补码方式"
/*****************************************************
* AES加密
* @param content 加密内容
* @param key 加密密码,由字母或数字组成
此方法使用AES-128-ECB加密模式,key需要为16位
加密解密key必须相同,如:abcd1234abcd1234
* @return 加密密文
****************************************************/
public static String enCode(String content, String key) {
if (key == null || "".equals(key)) {
System.out.println("key为空!");
return null;
}
if (key.length() != 16) {
System.out.println("key长度不是16位!");
return null;
}
try {
byte[] raw = key.getBytes(); //获得密码的字节数组
SecretKeySpec skey = new SecretKeySpec(raw, "AES"); //根据密码生成AES密钥
Cipher cipher = Cipher.getInstance(ALGORITHM); //根据指定算法ALGORITHM自成密码器
cipher.init(Cipher.ENCRYPT_MODE, skey); //初始化密码器,第一个参数为加密(ENCRYPT_MODE)或者解密(DECRYPT_MODE)操作,第二个参数为生成的AES密钥
byte [] byte_content = content.getBytes("utf-8"); //获取加密内容的字节数组(设置为utf-8)不然内容中如果有中文和英文混合中文就会解密为乱码
byte [] encode_content = cipher.doFinal(byte_content); //密码器加密数据
return Base64.encodeBase64String(encode_content); //将加密后的数据转换为字符串返回
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
/*****************************************************
* AES解密
* @param content 加密密文
* @param key 加密密码,由字母或数字组成
此方法使用AES-128-ECB加密模式,key需要为16位
加密解密key必须相同
* @return 解密明文
****************************************************/
public static String deCode(String content, String key) {
if (key == null || "".equals(key)) {
System.out.println("key为空!");
return null;
}
System.out.println(key.length()+"--------->>>>>");
if (key.length() != 16) {
System.out.println("key长度不是16位!");
return null;
}
try {
byte[] raw = key.getBytes(); //获得密码的字节数组
SecretKeySpec skey = new SecretKeySpec(raw, "AES"); //根据密码生成AES密钥
Cipher cipher = Cipher.getInstance(ALGORITHM); //根据指定算法ALGORITHM自成密码器
cipher.init(Cipher.DECRYPT_MODE, skey); //初始化密码器,第一个参数为加密(ENCRYPT_MODE)或者解密(DECRYPT_MODE)操作,第二个参数为生成的AES密钥
byte [] encode_content = Base64.decodeBase64(content); //把密文字符串转回密文字节数组
byte [] byte_content = cipher.doFinal(encode_content); //密码器解密数据
return new String(byte_content,"utf-8"); //将解密后的数据转换为字符串返回
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
/*****************************************************
* AES加密解密测试
* @param args
* @return
****************************************************/
// public static void main(String[] args) {
// String salt="dufy20119329java";
// String accpass= AesUtil.deCode("OWm2wB8ibExxGF7eZC0l/l7HjRsG5qNUbx02+j7gd7dzpFwj+Fj4lC5aOeFYTwEI",salt);
// System.out.println(accpass);
// }
}
全局异常处理类
package com.sykj.exception;
import com.sykj.util.ResponseResult;
import org.apache.shiro.authc.ExcessiveAttemptsException;
import org.apache.shiro.authc.IncorrectCredentialsException;
import org.apache.shiro.authc.LockedAccountException;
import org.apache.shiro.authc.UnknownAccountException;
import org.apache.shiro.authz.UnauthorizedException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
/**
* 全局异常处理类
*/
@RestControllerAdvice
public class CustomExceptionAdvice {
@ExceptionHandler(UnknownAccountException.class)
public ResponseResult xxx(UnknownAccountException e){
return new ResponseResult(-1000,"账号不存在,请重新输入",null);
}
@ExceptionHandler(IncorrectCredentialsException.class)
public ResponseResult password(IncorrectCredentialsException e){
return new ResponseResult(-1001,"密码错误,请重新输入",null);
}
@ExceptionHandler(ClassCastException.class)
public ResponseResult password(ClassCastException e){
return new ResponseResult(-1024,e.getMessage(),null);
}
@ExceptionHandler(UnauthorizedException.class)
public ResponseResult aa(UnauthorizedException e){
return new ResponseResult(-1000,"权限不足,亲联系管理员",null);
}
@ExceptionHandler(LockedAccountException.class)
public ResponseResult zzz(LockedAccountException e){
System.out.println("===============================================");
return new ResponseResult(-1002,e.getMessage(),null);
}
@ExceptionHandler(ExcessiveAttemptsException.class)
public ResponseResult eee(ExcessiveAttemptsException e){
return new ResponseResult(-1003,e.getMessage(),null);
}
}
服务器响应类
package com.sykj.util;
import java.io.Serializable;
/**
* 服务器响应类
*/
public class ResponseResult implements Serializable {
private int code;
private String message;
private Object data;
public ResponseResult(int code, String message, Object data) {
this.code = code;
this.message = message;
this.data = data;
}
public ResponseResult() {
}
public int getCode() {
return code;
}
public void setCode(int code) {
this.code = code;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
public Object getData() {
return data;
}
public void setData(Object data) {
this.data = data;
}
}
AccountService类
package com.sykj.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.sykj.bean.Account;
public interface AccountService extends IService<Account> {
}
AccountServiceImpl类
package com.sykj.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.sykj.bean.Account;
import com.sykj.mapper.AccountMapper;
import com.sykj.service.AccountService;
import org.springframework.stereotype.Service;
@Service
public class AccountServiceImpl extends ServiceImpl<AccountMapper, Account> implements AccountService {
}
PermissionService类
package com.sykj.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.sykj.bean.Permission;
import java.util.List;
public interface PermissionService extends IService<Permission> {
public List<Permission> querypermiss(Integer rid);
}
PermissionServiceImpl类
package com.sykj.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.fasterxml.jackson.annotation.ObjectIdGenerators;
import com.sykj.bean.Permission;
import com.sykj.mapper.PermissionMapper;
import com.sykj.service.PermissionService;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class PermissionServiceImpl extends ServiceImpl<PermissionMapper, Permission> implements PermissionService {
public List<Permission> querypermiss(Integer rid) {
return this.baseMapper.querypermiss(rid);
}
}
Permission.xml文件
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.sykj.mapper.PermissionMapper">
<select id="querypermiss" parameterType="integer" resultType="com.sykj.bean.Permission">
SELECT p.*
FROM role_permission rp,permission p
where rp.pid=p.pid and rp.rid=#{rid}
</select>
</mapper>
RoleService类
package com.sykj.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.sykj.bean.Role;
import java.util.List;
public interface RoleService extends IService<Role> {
public List<Role> queryRoles(Integer accid);
}
RoleServiceImpl类
package com.sykj.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.sykj.bean.Role;
import com.sykj.mapper.RoleMapper;
import com.sykj.service.RoleService;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class RoleServiceImpl extends ServiceImpl<RoleMapper, Role> implements RoleService {
@Override
public List<Role> queryRoles(Integer accid) {
return this.baseMapper.queryRoles(accid);
}
}
Role.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.sykj.mapper.RoleMapper">
<select id="queryRoles" parameterType="integer" resultType="com.sykj.bean.Role">
select r.* from account_role ar,role r where r.rid=ar.rid and ar.accid=#{accid}
</select>
</mapper>
AccountController
package com.sykj.controller;
import com.sykj.util.ResponseResult;
import com.sykj.util.AesUtil;
import com.sykj.util.NoRepeatSubmit;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.apache.shiro.subject.Subject;
import org.springframework.web.bind.annotation.*;
@CrossOrigin
@RestController
public class AccountController {
@RequestMapping("/user/login")
public ResponseResult login(@RequestParam("username") String username,@RequestParam("password") String password){
Subject subject= SecurityUtils.getSubject();//用户主体
System.out.println(password+"......>>>>>>>>>>>>>>>>>>>>>>>>>>>...");
String password1=AesUtil.deCode(password,"sykj123456789111");
System.out.println(password1+"===========");
UsernamePasswordToken token=new UsernamePasswordToken(username,password1);
subject.login(token);//流程走向Realm
return new ResponseResult(1000,"登陆成功",null);
}
@GetMapping("user/delete")
public ResponseResult delete(){
return new ResponseResult(1000,"删除成功",null);
}
@GetMapping("user/save")
public ResponseResult save(){
return new ResponseResult(1000,"添加成功",null);
}
@GetMapping("user/query")
public ResponseResult query(){
return new ResponseResult(1000,"查询成功",null);
}
@GetMapping("user/update")
public ResponseResult update(){
return new ResponseResult(1000,"修改成功",null);
}
}
至此大功告成 !!!