Shiro
1. Shiro简介
1.1 什么是Shiro
- Apache Shiro是一个Java的安全(权限)框架
- 不仅可以用在JavaSE环境,也可以用在JavaEE环境
- Shiro可以完成:认证、授权、加密、会话管理、Web集成、缓存等
- 下载地址
1.2 功能
- Authentication:身份认证、登录、验证用户是不是拥有相应的身份;
- Authorization:授权,即权限验证,验证某个已认证的用户是否拥有某个权限,即判断用户能进行什么操作;
- Session Manager:会话管理,即用户登录后就是第一次会话,在没有退出前,它的所有信息都在会话中。会话可以是普通的JavaSE环境,也可以是Web环境;
- Cryptography:加密,保护数据的安全性。如密码加密存储到数据库中,而不是明文存储;
- Web Support:Web支持,可以非常容易的集成到Web环境;
- Caching:缓存,比如用户登录后,其用户信息,拥有的角色、权限不必每次去查,可以提高效率
- Concurrency:Shiro支持多线程应用的并发验证。即,如在一个线程中开启另一个线程,能把权限自动的传播过去
- Testing:提供测试支持;
- Run As:允许一个用户假装为另一个用户(如果他们允许)的身份进行访问;
- Remember Me:记住我,即一次登陆后,下次再来的话就不用登录了
1.3 架构
外部架构:
- subject:应用代码直接交互的对象是Subject,也就是说Shiro的对外核心API就是Subject,Subject代表来当前的用户,这个用户不一定是一个具体的人,与当前应用交互的任何东西都是Subject,如网络爬虫,机器人等,与Subject的所有交互都会委托给SecurityManager。Subject其实是一个门面,SecurityManager才是实际的执行者
- SecurityManager:安全管理器,**即所有与安全有关的操作都会与SecurityManager交互,并且它管理着所有的Subject,**可以看出它是Shiro的核心,它负责与Shiro的其它组件进行交互,相当于SpringMVC中的DispathcerServlet角色
- Realm:Shiro从Realm获取安全数据(如用户、角色、权限),就是说SecurityManager要验证用户身份,那么它需要从Realm获取相应的用户进行比较,来确定用户的身份是否合法;也需要从Realm得到用户相应的角色、权限,进行验证用户的操作是否能够进行,可以把Realm看成DataSource
内部架构:
- Subject:任何可以与应用交互的’用户’;
- Security Manager:相当于SpringMVC中的DispatcherServlet。是Shiro的心脏,所有具体的交互都通过Security Manager进行控制,它管理着所有的Subject,且负责认证、授权、会话、及缓存的管理;
- Authenticator:负责Subject认证,是一个扩展点,可以自定义实现。可以使用认证策略(Authentication Strategy),即怎么样才算用户通过认证;
- Authorizer:授权器,即访问控制器,用来决定主体是否有权限进行相应的操作;即控制着用户能访问应用中的哪些功能;
- Realm:可以有一个或者多个的realm,可以认为是安全实体数据源,即用于获取安全实体的,可以用JDBC实现,也可以是内存实现等等。由用户提供,所以一般在应用中都需要实现自己的realm;
- Session Manager:管理Session生命周期的组件,而Shiro并不仅仅可以用在Web环境,也可以用在普通的JavaSE环境中;
- Cache Manager:缓存控制器,来管理如用户,角色,权限等缓存的;因为这些数据基本上很少改变,放到缓存中后可以提高访问的性能;
- Cryptography:密码模块,Shiro提高了一些常见的加密组件用于密码加密,解密等
2. Hello World
2.1 快速实践
查看官方文档:官方链接
官方的quick start:直达链接
-
创建maven项目
-
导入shiro的依赖
<dependencies> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-core</artifactId> <version>1.11.0</version> </dependency> <!-- configure logging --> <dependency> <groupId>org.slf4j</groupId> <artifactId>jcl-over-slf4j</artifactId> <version>1.7.24</version> </dependency> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-simple</artifactId> <version>1.7.24</version> </dependency> </dependencies>
-
导入QuickStart类
import org.apache.shiro.SecurityUtils; import org.apache.shiro.authc.*; import org.apache.shiro.mgt.SecurityManager; import org.apache.shiro.session.Session; import org.apache.shiro.subject.Subject; import org.apache.shiro.config.IniSecurityManagerFactory; import org.apache.shiro.util.Factory; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Simple Quickstart application showing how to use Shiro's API. * * @since 0.9 RC2 */ public class Quickstart { private static final transient Logger log = LoggerFactory.getLogger(Quickstart.class); public static void main(String[] args) { // The easiest way to create a Shiro SecurityManager with configured // realms, users, roles and permissions is to use the simple INI config. // We'll do that by using a factory that can ingest a .ini file and // return a SecurityManager instance: // Use the shiro.ini file at the root of the classpath // (file: and url: prefixes load from files and urls respectively): Factory<SecurityManager> factory = new IniSecurityManagerFactory("classpath:shiro.ini"); SecurityManager securityManager = factory.getInstance(); // for this simple example quickstart, make the SecurityManager // accessible as a JVM singleton. Most applications wouldn't do this // and instead rely on their container configuration or web.xml for // webapps. That is outside the scope of this simple quickstart, so // we'll just do the bare minimum so you can continue to get a feel // for things. SecurityUtils.setSecurityManager(securityManager); // Now that a simple Shiro environment is set up, let's see what you can do: // get the currently executing user: Subject currentUser = SecurityUtils.getSubject(); // Do some stuff with a Session (no need for a web or EJB container!!!) Session session = currentUser.getSession(); session.setAttribute("someKey", "aValue"); String value = (String) session.getAttribute("someKey"); if (value.equals("aValue")) { log.info("Retrieved the correct value! [" + value + "]"); } // let's login the current user so we can check against roles and permissions: if (!currentUser.isAuthenticated()) { UsernamePasswordToken token = new UsernamePasswordToken("lonestarr", "vespa"); token.setRememberMe(true); try { currentUser.login(token); } catch (UnknownAccountException uae) { log.info("There is no user with username of " + token.getPrincipal()); } catch (IncorrectCredentialsException ice) { log.info("Password for account " + token.getPrincipal() + " was incorrect!"); } catch (LockedAccountException lae) { log.info("The account for username " + token.getPrincipal() + " is locked. " + "Please contact your administrator to unlock it."); } // ... catch more exceptions here (maybe custom ones specific to your application? catch (AuthenticationException ae) { //unexpected condition? error? } } //say who they are: //print their identifying principal (in this case, a username): log.info("User [" + currentUser.getPrincipal() + "] logged in successfully."); //test a role: if (currentUser.hasRole("schwartz")) { log.info("May the Schwartz be with you!"); } else { log.info("Hello, mere mortal."); } //test a typed permission (not instance-level) if (currentUser.isPermitted("lightsaber:wield")) { log.info("You may use a lightsaber ring. Use it wisely."); } else { log.info("Sorry, lightsaber rings are for schwartz masters only."); } //a (very powerful) Instance Level permission: if (currentUser.isPermitted("winnebago:drive:eagle5")) { log.info("You are permitted to 'drive' the winnebago with license plate (id) 'eagle5'. " + "Here are the keys - have fun!"); } else { log.info("Sorry, you aren't allowed to drive the 'eagle5' winnebago!"); } //all done - log out! currentUser.logout(); System.exit(0); } }
-
配置文件
log4j.properties
log4j.rootLogger=ERROR, stdout log4j.appender.stdout=org.apache.log4j.ConsoleAppender log4j.appender.stdout.layout=org.apache.log4j.PatternLayout log4j.appender.stdout.layout.ConversionPattern=%d %p [%c] - %m %n # Pattern to output: date priority [category] - message log4j.appender.logfile.layout=org.apache.log4j.PatternLayout log4j.appender.logfile.layout.ConversionPattern=%d %p [%c] - %m%n # Spring logging level is WARN log4j.logger.org.springframework=WARN # General Apache libraries is WARN log4j.logger.org.apache=WARN log4j.logger.net.sf.ehcache=WARN log4j.logger.org.apache.shiro=INFO log4j.logger.org.apache.shiro.util.ThreadContext=WARN
shiro.ini
[users] # user 'root' with password 'secret' and the 'admin' role root = secret, admin # user 'guest' with the password 'guest' and the 'guest' role guest = guest, guest # user 'presidentskroob' with password '12345' ("That's the same combination on # my luggage!!!" ;)), and role 'president' presidentskroob = 12345, president # user 'darkhelmet' with password 'ludicrousspeed' and roles 'darklord' and 'schwartz' darkhelmet = ludicrousspeed, darklord, schwartz # user 'lonestarr' with password 'vespa' and roles 'goodguy' and 'schwartz' lonestarr = vespa, goodguy, schwartz # ----------------------------------------------------------------------------- # Roles with assigned permissions # # Each line conforms to the format defined in the # org.apache.shiro.realm.text.TextConfigurationRealm#setRoleDefinitions JavaDoc # ----------------------------------------------------------------------------- [roles] # 'admin' role has all permissions, indicated by the wildcard '*' admin = * # The 'schwartz' role can do anything (*) with any lightsaber: schwartz = lightsaber:* # The 'goodguy' role is allowed to 'drive' (action) the winnebago (type) with # license plate 'eagle5' (instance specific id) goodguy = winnebago:drive:eagle5
2.2 spring整合shiro
1. 导入依赖
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.11.0</version>
</dependency>
2. 编写配置类
@Configuration
public class ShiroConfig {
//1. 创建 realm 对象,需要自定义类
@Bean
public UserRealm userRealm() {
//把自定义类注入到bean里,让spring管理这个类
return new UserRealm();
}
//2. DefaultWebSecureManager
@Bean(name="securityManager")
public DefaultWebSecurityManager getDefaultWebSecurityManager(@Qualifier("userRealm") UserRealm userRealm) {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
//关联UserRealm
securityManager.setRealm(userRealm);
return securityManager;
}
//3. ShiroFilterFactoryBean shiro过滤的一些对象
@Bean
public ShiroFilterFactoryBean getShiroFilterFactoryBean(@Qualifier("securityManager") DefaultWebSecurityManager defaultWebSecurityManager) {
ShiroFilterFactoryBean factoryBean = new ShiroFilterFactoryBean();
//设置安全管理器
factoryBean.setSecurityManager(defaultWebSecurityManager);
return factoryBean;
}
}
其中UserRealm是自定义类
//自定义的UserRealm, 需要继承 AuthorizingRealm
public class UserRealm extends AuthorizingRealm {
//授权
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
System.out.println("执行了=>授权doGetAuthorizationInfo");
return null;
}
//认证
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
System.out.println("执行了=>认证doGetAuthenticationInfo");
return null;
}
}
3. 进一步配置
以上还只是把环境给搭建好了,要实现各种功能还需先配置shiro的内置过滤器
ShiroFilterFactoryBean | 限制 |
---|---|
authc | 必须认证了才能访问 |
user | 必须拥有 记住我 功能才能访问 |
perms | 拥有对某个资源的权限才能访问 |
roles | 拥有某个角色的权限才能访问 |
anon | 无需认证就可以访问 |
更新getShiroFilterFactoryBean()方法,添加内置过滤器配置和设置登录页请求,进行拦截
@Bean
public ShiroFilterFactoryBean getShiroFilterFactoryBean(@Qualifier("securityManager") DefaultWebSecurityManager defaultWebSecurityManager) {
ShiroFilterFactoryBean filterBean = new ShiroFilterFactoryBean();
//设置安全管理器
filterBean.setSecurityManager(defaultWebSecurityManager);
//添加内置过滤器配置
Map<String, String> filterMap = new LinkedHashMap<>();
filterMap.put("/user/*", "authc"); //认证了才能访问/user下的资源
filterBean.setFilterChainDefinitionMap(filterMap);
//设置登录的请求,没有认证的跳到登陆页
filterBean.setLoginUrl("/toLogin");
return filterBean;
}
4. 认证
先是Controller接收前端发送的用户名和密码
@RequestMapping("/login")
public String login(String username, String password, Model model) {
//获取当前用户
Subject subject = SecurityUtils.getSubject();
//封装用户登录数据
UsernamePasswordToken token = new UsernamePasswordToken(username, password);
try {
subject.login(token); //执行登录方法,如果没异常说明成功
return "index";
} catch (UnknownAccountException e) { //用户名不存在
model.addAttribute("msg", "用户名不存在");
return "login";
} catch (IncorrectCredentialsException e) { //密码错误
model.addAttribute("msg", "密码错误");
return "login";
}
}
然后UserRealm中编写认证,可以从数据库中读取数据,这里从简
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
System.out.println("执行了=>认证doGetAuthenticationInfo");
//realm,subject和securityManager都是有联系的,在Controller中subject用了token,这里也能获取得到
UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken;
//可以从数据库中去用户名、密码出来进行匹对,这里粗定义数据
String username = "root";
String password = "123456";
if(!token.getUsername().equals(username)) {
return null; //return null会抛出 UnknownAccountException
}
//由于密码涉及安全问题,所以密码认证部分shiro已经完成,将密码丢给认证就行
return new SimpleAuthenticationInfo("",password,"");
}
先连接数据库,导入四个依赖
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.17</version>
</dependency>
<!-- mysql依赖 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.11</version>
</dependency>
<!-- druid依赖 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.2.15</version>
</dependency>
<!-- mybatis依赖 -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.2.2</version>
</dependency>
配置druid(application.yml)
spring:
datasource:
name: mysql_test
type: com.alibaba.druid.pool.DruidDataSource
#druid相关配置
druid:
#监控统计拦截的filters
filters: stat
driver-class-name: com.mysql.jdbc.Driver
#基本属性
url: jdbc:mysql://127.0.0.1:3306/m?useUnicode=true&characterEncoding=UTF-8&setTimezone=UTC
username: root
password: 123456
#配置初始化大小/最小/最大
initial-size: 1
min-idle: 1
max-active: 20
#获取连接等待超时时间
max-wait: 60000
#间隔多久进行一次检测,检测需要关闭的空闲连接
time-between-eviction-runs-millis: 60000
#一个连接在池中最小生存的时间
min-evictable-idle-time-millis: 300000
validation-query: SELECT 'x'
test-while-idle: true
test-on-borrow: false
test-on-return: false
#打开PSCache,并指定每个连接上PSCache的大小。oracle设为true,mysql设为false。分库分表较多推荐设置为false
pool-prepared-statements: false
max-pool-prepared-statement-per-connection-size: 20
配置mybatis(application.properties)
mybatis.type-aliases-package=com.rodrigol.pojo
mybatis.mapper-locations=classpath:mapper/*.xml
UserMapper.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.rodrigol.mapper.UserMapper">
<select id="selectUser" resultType="User">
select * from user
</select>
<select id="selectUserById" resultType="User">
select * from user where id = #{id}
</select>
<insert id="addUser" parameterType="User">
insert into user(id, name, pwd) VALUES (#{id},#{name},#{pwd})
</insert>
<update id="updateUser" parameterType="User">
update user set name = #{name}, pwd = #{pwd} where id = #{id}
</update>
<delete id="deleteUser" parameterType="int">
delete from user where id = #{id}
</delete>
<select id="queryUserByName" resultType="User">
select * from user where name = #{name}
</select>
</mapper>
在UserRealm中添加Bean(userService),利用其来连接数据库
@Autowired
UserServiceImpl userService;
修改过后的doGetAuthenticationInfo()方法
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
System.out.println("执行了=>认证doGetAuthenticationInfo");
//realm,subject和securityManager都是有联系的,在Controller中subject用了token,这里也能获取得到
UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken;
//从数据库中取数据
User user = userService.queryUserByName(token.getUsername());
if(user == null) { return null; }
//由于密码涉及安全问题,所以密码认证部分shiro已经完成,将密码丢给认证就行
return new SimpleAuthenticationInfo("",user.getPwd(),"");
}
5. 授权
在过滤器中设置拥有对某个资源的权限才能访问,在ShiroConfig的getShiroFilterFactoryBean()方法中
@Bean
public ShiroFilterFactoryBean getShiroFilterFactoryBean(@Qualifier("securityManager") DefaultWebSecurityManager defaultWebSecurityManager) {
······
//授权
filterMap.put("/user/add", "perms[user:add]");
filterMap.put("/user/update", "perms[user:update]");
//设置授权的界面,未授权的会跳转到提示页
filterBean.setUnauthorizedUrl("/unauth");
······
return filterBean;
}
tips:设置未授权提示页面(setUnauthorizedUrl)和未登录跳转页面(setLoginUrl),最好是授权在先,登录在后
然后在UserRealm的**doGetAuthorizationInfo(PrincipalCollection principalCollection)**方法中给用户设置权限
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
System.out.println("执行了=>授权doGetAuthorizationInfo");
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
//info.addStringPermission("user:add"); 硬编码,每个用户都会被加上这个权限,不安全
//拿到当前登录的这个对象,查数据库看有什么权限
Subject subject = SecurityUtils.getSubject();
User currentUser = (User) subject.getPrincipal(); //这里是用getPrincipal获取,在认证中,需要把对象传过来
//设置权限,当前用户具有的权限,数据库查到的
info.addStringPermission(currentUser.getPerms());
return info;
}
用户登录的时候可以认证其身份,根据他所具有的权限开放或关闭某些资源。