这几天在学Apache的shiro,现在简单记录下学习过程,以及spring boot简单的整合shiro例子
shiro
Apache Shiro 是一个功能强大、灵活的,开源的安全框架。它可以干净利落地处理身份验证、授权、企业会话管理和加密。Shiro 可以非常容易的开发出足够好的应用,其不仅可以用在 JavaSE 环境,也可以用在 JavaEE 环境。Shiro 可以帮助我们完成:认证、授权、加密、会话管理、与 Web 集成、缓存等。而且 Shiro 的 API 也是较为简单;
Shiro Features 特性
Apache Shiro 是一个全面的、蕴含丰富功能的安全框架。Authentication(认证), Authorization(授权), Session Management(会话管理), Cryptography(加密)为 Shiro的四大基石。
- Authentication(认证):用户身份识别,通常被称为用户“登录”
- Authorization(授权): 访问控制。比如某个用户是否具有某个操作的使用权限。
- Session Management(会话管理):特定于用户的会话管理,甚至在非web 或 EJB 应用程序。
- Cryptography(加密): 在对数据源使用加密算法加密的同时,保证易于使用。
shiro架构
shiro架构主要包括Subject,SecurityManager,Realm
- Subject: 当前用户
- SecurityManager: 管理所有Subject,SecurityManager 是 Shiro 架构的核心,配合内部安全组件共同组成安全伞。
- Realms: 用于进行权限信息的验证,我们自己实现。Realm 本质上是一个特定的安全 DAO:它封装与数据源连接的细节,得到Shiro 所需的相关的数据。在配置 Shiro 的时候,你必须指定至少一个Realm 来实现认证(authentication)和/或授权(authorization)。
Tips: Shiro 不会去维护用户、维护权限,这些需要我们自己去设计/提供,然后通过相应的接口注入给 Shiro
SpringBoot整合shiro
pom文件
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<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>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
数据库配置
spring:
datasource:
username: root
password: 1001101
url: jdbc:mysql://localhost:3306/shiro?serverTimezone=GMT%2B8
initialization-mode: always
driver-class-name: com.mysql.cj.jdbc.Driver
jpa:
hibernate:
ddl-auto: update
show-sql: true
database: mysql
thymeleaf:
cache: false
mode: HTML
本案例使用RBAC设计
使用JPA来自动生成表,对于的实体类如下:
用户表
@Entity
public class User {
@Id
@GeneratedValue
private int id;
@Column(unique = true)
private String name;
private String password;
@ManyToMany(fetch = FetchType.EAGER)
@JoinTable(name = "UserRole",joinColumns = {@JoinColumn(name = "uid")},inverseJoinColumns = {@JoinColumn(name = "roleId")})
private List<Role> roleList;//一个用户有多个角色
//get,set方法省略,下同
角色表
@Entity
public class Role {
@Id
@GeneratedValue
private int id;
private String role;
private String description;
@ManyToMany
@JoinTable(name="UserRole",joinColumns={@JoinColumn(name="roleId")},inverseJoinColumns={@JoinColumn(name="uid")})
private List<User> userInfos;// 一个角色对应多个用户
@ManyToMany(fetch= FetchType.EAGER)
@JoinTable(name="RolePermission",joinColumns={@JoinColumn(name="roleId")},inverseJoinColumns={@JoinColumn(name="permissionId")})
private List<Premisson> permissions;
权限表
@Entity
public class Premisson {
@Id
@GeneratedValue
private int id;
private String name;//名称
private String url;
private String permission;
@ManyToMany
@JoinTable(name="RolePermission",joinColumns={@JoinColumn(name="permissionId")},inverseJoinColumns={@JoinColumn(name="roleId")})
private List<Role> Roles;
运行后数据库会生成五张表
为了方便测试,我们要往其中添加一些数据进行测试,所有的sql语句我会放在GitHub中
shiroConfig
@Configuration
public class ShiroConfig {
@Bean
public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager){
System.out.println("shiro过滤器");
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
shiroFilterFactoryBean.setSecurityManager(securityManager);
Map<String,String> filterMap = new LinkedHashMap<>();
//anon为匿名访问不需要认证即可访问,authc需要认证才可访问
filterMap.put("/static/**","anon" );
filterMap.put("/regi","anon" );
filterMap.put("/register","anon" );
filterMap.put("/logout", "logout");//shiro内部自动帮我们执行退出,移除session,然后跳转到.setLoginUrl配置的页面
filterMap.put("/login.action","anon" );
//一般这个都放在最后一个
filterMap.put("/**","authc" );
// 如果不设置默认会自动寻找Web工程根目录下的"/login.jsp"页面
shiroFilterFactoryBean.setLoginUrl("/login");
// shiroFilterFactoryBean.setUnauthorizedUrl("403");
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterMap);
return shiroFilterFactoryBean;
}
/**
* 自定义加密策略,加密算法为MD5,加密次数为2次
* @return
*/
@Bean
public HashedCredentialsMatcher hashedCredentialsMatcher(){
HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
hashedCredentialsMatcher.setHashAlgorithmName("md5");
hashedCredentialsMatcher.setHashIterations(2);
return hashedCredentialsMatcher;
}
/**
* 设置加密策略为自定义的加密策略
* @return
*/
@Bean
public MyShiroRealm myShiroRealm(){
MyShiroRealm myShiroRealm = new MyShiroRealm();
myShiroRealm.setCredentialsMatcher(hashedCredentialsMatcher());
return myShiroRealm;
}
@Bean
public SecurityManager securityManager(){
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(myShiroRealm());
return securityManager;
}
/**
* 开启shiro aop注解支持.
* 使用代理方式;所以需要开启代码支持;
* @param securityManager
* @return
*/
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager){
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
return authorizationAttributeSourceAdvisor;
}
}
- anon:所有 url 都都可以匿名访问
- authc: 需要认证才能进行访问
- user:配置记住我或认证通过可以访问
自定义Realm
认证的实现
因为在 Shiro 中,最终是通过 Realm 来获取应用程序中的用户、角色及权限信息的。通常情况下,在 Realm 中会直接从我们的数据源中获取 Shiro 需要的验证信息。可以说,Realm 是专用于安全框架的 DAO. Shiro 的认证过程最终会交由 Realm 执行,当执行subject.login时就会调用 Realm 的doGetAuthenticationInfo(AuthenticationToken token) 方法进行认证
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
System.out.println("-----认证----");
UsernamePasswordToken token1 = (UsernamePasswordToken) token;
String name = token1.getUsername();//获得前端用户输入的名字
User user = userDao.findByName(name);//从数据库查询,返回用户
if(null == user){
return null;
}
Object salt = ByteSource.Util.bytes(name);//加用户名为盐
return new SimpleAuthenticationInfo(user,user.getPassword(), (ByteSource) salt,getName());
}
这里使用的MD5加盐对密码进行加密,盐一般设置为用户名,这样即使密码一样,最后存入到数据库的密文也是不同的
new SimpleAuthenticationInfo(user,user.getPassword(), (ByteSource) salt,getName()); 对密码进行检验
执行过程图解
权限认证
Shiro 的权限授权是通过Realm重载 doGetAuthorizationInfo(); 方法,当访问到页面的时候,链接配置了相应的权限或者 Shiro 标签才会执行此方法否则不会执行,是个懒加载。SimpleAuthorizationInfo 进行角色的添加和权限的添加。
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
System.out.println("权限配置-->MyShiroRealm.doGetAuthorizationInfo()");
SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
User user = (User) principals.getPrimaryPrincipal();
for(Role role:user.getRoleList()){//从当前的用户获取该用户具有的所有角色
authorizationInfo.addRole(role.getRole());
for(Premisson p:role.getPermissions()){
authorizationInfo.addStringPermission(p.getPermission());//添加权限
}
}
return authorizationInfo;
}
注册用户
当用户注册时,对密码的加密算法要与shiro的相同,否则认证的时候永远都是密码错误
@RequestMapping("/register")
public String register(User user){
System.out.println(user.getName()+"---"+user.getPassword());
String password = MD5Utils.md5(user);
user.setPassword(password);
userDao.save(user);
return "login";
}
//加密算法
public class MD5Utils {
public static String md5(User user){
String name = "MD5";//MD5加密
String pwd = user.getPassword();
ByteSource salt = ByteSource.Util.bytes(user.getName());//用户名为盐值
int num = 2;//加密次数
Object result = new SimpleHash(name,pwd ,salt ,num );
System.out.println(result);
return result.toString();
}
}
登录
subject.login进行登录认证
@RequestMapping("/login.action")
public String login(String username, String password, Model model, HttpSession session){
Subject subject = SecurityUtils.getSubject();
//new一个 UsernamePasswordToken,并传上用户名及密码。把返回值传给登入作为条件。
UsernamePasswordToken token = new UsernamePasswordToken(username,password );
try{
subject.login(token);
System.out.println(subject.isAuthenticated());
User user = (User) subject.getPrincipal();
session.setAttribute("user",user );
return "main";
}catch (UnknownAccountException e){
model.addAttribute("msg","用户名错误" );
return "login";
}catch (IncorrectCredentialsException e){
model.addAttribute("msg","密码错误" );
return "login";
}
}
异常处理类
当产生UnauthorizedException异常时,即某个用户访问某个没有权限的页面时,返回403页面
@ControllerAdvice
public class ExceptionHandler {
@org.springframework.web.bind.annotation.ExceptionHandler(UnauthorizedException.class)
public ModelAndView exc(){
ModelAndView modelAndView = new ModelAndView();
modelAndView.setViewName("403");
return modelAndView;
}
}
运行程序
注册页面
数据库保存的密文
登录
因为为普通用户,所有只有用户查看的权限
换成admin用户就要除了vip的所有权限
退出
完整的代码在我的GitHub中