Shiro基础知识点

  • Apache Shiro 是ASF旗下的一款开源软件(Shiro发音为“shee-roh”,日语“堡垒(Castle)”的意思),提供了一个强大而灵活的安全框架。可为任何应用提供安全保障— 从命令行应用、移动应用到大型网络及企业应用。

  • Apache Shiro提供了认证、授权、加密和会话管理功能,将复杂的问题隐藏起来,提供清晰直观的API使开发者可以很轻松地开发自己的程序安全代码。并且在实现此目标时无须依赖第三方的框架、容器或服务,当然也能做到与这些环境的整合,使其在任何环境下都可拿来使用。

Shiro的核心四部分

  • 认证(Authentication):用户身份识别。有时可看作为“登录(login)”,它是用户证明自己是谁的一个行为。

  • 授权(Authorization):访问控制过程,好比决定“认证(who)”可以访问“什么(what)”.

  • 会话管理(SessionManagement):管理用户的会话(sessions),甚至在没有WEB或EJB容器的环境中。管理用户与时间相关的状态。

  • 加密(Cryptography):使用加密算法保护数据更加安全,防止数据被偷窥。

1.Spring整合Shiro

  1. pom 依赖
    1
    2
    3
    4
    5
    6
    <!--整合shiro-->
    <dependency>
    <groupId>org.apache.shiro</groupId>
    <artifactId>shiro-spring-boot-starter</artifactId>
    <version>1.4.1</version>
    </dependency>

2. ShiroConfig

  • shiro的基本配置
  • 1.ShiroFilterFactoryBean #Shiro对外的接口,代表当前“用户”
  • 2.DefaultWebSecurityManager #关联realm
  • 3.UserRealm #负责登录验证和资源授权
  • 1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    package io.kid1999.operatesystem.config;

    import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
    import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
    import org.springframework.beans.factory.annotation.Qualifier;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;

    import java.util.LinkedHashMap;
    import java.util.Map;

    /**
    * @author kid1999
    * @title: Shiro 的 配置
    * @date 2019/11/26 21:27
    */

    @Configuration
    public class ShiroConfig {

    // 3. 创建 ShiroFilterFactoryBean
    @Bean
    public ShiroFilterFactoryBean getShiroFilterFactoryBean(@Qualifier("securityManager") DefaultWebSecurityManager securityManager){
    ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
    // 设置安全管理器
    shiroFilterFactoryBean.setSecurityManager(securityManager);

    /** 添加Shiro的内置过滤器
    * 常用:
    * anon : 无需认证(登录) 可以访问
    * authc :必须认证才可以访问
    * user : 如果使用remenberMe功能可以直接访问
    * perms: 该资源必须获得相关权限才可访问
    * role: 该资源必须获得角色权限才可以访问
    *
    * 注意: 这个授权拦截链 是按顺序执行的!!!
    */
    Map<String,String> filterMap = new LinkedHashMap<>();
    filterMap.put("/","anon");
    filterMap.put("/login","anon");
    filterMap.put("/error","anon");
    // filterMap.put("/admin","authc");

    // 权限过滤器 也可以设置角色过滤
    filterMap.put("/admin","perms[user:admin]");

    shiroFilterFactoryBean.setLoginUrl("/login"); // 设置跳转的登录页面
    shiroFilterFactoryBean.setUnauthorizedUrl("/noAuth"); // 设置未授权访问页面
    shiroFilterFactoryBean.setFilterChainDefinitionMap(filterMap);

    return shiroFilterFactoryBean;
    }

    // 2. 创建DefaultWebSecurityManager
    // 所有与安全有关的操作都会与SecurityManager进行交互
    @Bean(name = "securityManager")
    public DefaultWebSecurityManager getDefaultWebSecurityManager(@Qualifier("getRealm") UserRealm userRealm){
    DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
    // 关联realm
    securityManager.setRealm(getRealm());
    return securityManager;
    }

    // 1. 创建Realm
    // Shiro从Realm获取安全数据(用户、角色、权限)
    @Bean
    public UserRealm getRealm(){
    return new UserRealm();
    }
    }

3. UserRealm

  • Shiro从从Realm获取安全数据(如用户、角色、权限)
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    package io.kid1999.operatesystem.config;

    import io.kid1999.operatesystem.model.Admin;
    import io.kid1999.operatesystem.repository.AdminRepository;
    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;

    /**
    * @author kid1999
    * @title: UserRealm 自定义 realm 处理 授权和认证
    * @date 2019/11/26 21:29
    */



    public class UserRealm extends AuthorizingRealm {
    @Autowired
    private AdminRepository adminRepository;

    // 授权执行逻辑
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
    System.out.println("授权执行逻辑");

    // 给资源进行授权
    SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();

    // 添加资源授权字符串
    info.addStringPermission("user:admin");

    return info;
    }

    // 认证执行逻辑
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
    System.out.println("认证执行逻辑");

    // 1. 获取传来验证的token
    UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken;
    String name = token.getUsername();
    String password = String.valueOf(token.getPassword());
    // 2. 通过用户名 取出数据库的数据
    Admin user = adminRepository.findByAdminId(name);

    // 3. 判断用户名
    if (user == null){
    return null; // 找不到用户名 报 UnknownAccountException
    }

    // 4. 判断密码
    /**
    * arg0: 回传login的数据
    * arg1: 数据库的密码
    * arg2: realm 的name
    */
    return new SimpleAuthenticationInfo("",user.getAdminPwd(),"");
    }
    }

4.AdminRepository 链接数据库

  • Realm 会连接数据源 进行身份验证
  • 此处放上我的JPA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package io.kid1999.operatesystem.repository;
import io.kid1999.operatesystem.model.Admin;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;

/**
* @author kid1999
* @title: ManagerRepository
* @date 2019/11/24 10:07
*/
public interface AdminRepository extends JpaRepository<Admin,Integer> {
@Query(value = "select * from admin m where m.admin_id = ?1 and m.admin_pwd = ?2" ,nativeQuery = true)
Admin login(String studentId, String passWord);

Admin findByAdminId(String adminId);
}

5.登录Controller

  • 此处没有使用加密 和 缓存机制
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    /**
    * 使用Shiro实现登录
    */
    @PostMapping("/login")
    String toLogin(String studentId,
    String passWord,
    Model model){
    // 1.获取Subject
    Subject subject = SecurityUtils.getSubject();
    // 2.封装登录方法
    UsernamePasswordToken token = new UsernamePasswordToken(studentId,passWord);
    // 3.执行登录方法
    try {
    subject.login(token); // 调用login方法 -> realm 做验证
    return "admin";
    }catch (UnknownAccountException e){
    model.addAttribute("msg","用户名不存在");
    return "login";
    }catch (IncorrectCredentialsException e){
    model.addAttribute("msg","密码错误");
    return "login";
    }
    }

6.整合redis接管session和缓存

  1. 再导入两个包
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-ehcache</artifactId>
<version>1.4.1</version>
</dependency>

<!-- shiro+redis缓存插件 -->
<dependency>
<groupId>org.crazycake</groupId>
<artifactId>shiro-redis</artifactId>
<version>3.2.3</version>
<!-- 剔除这个错误依赖 -->
<exclusions>
<exclusion>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
</exclusion>
</exclusions>
</dependency>
  1. 在ShiroConfig 配置到 securityManager
1
2
3
4
5
6
7
8
9
10
11
12
// 2. 创建DefaultWebSecurityManager  管理器
@Bean(name = "securityManager")
public DefaultWebSecurityManager getDefaultWebSecurityManager(@Qualifier("getRealm") UserRealm userRealm){
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
// 关联realm
securityManager.setRealm(getRealm());
// 自定义缓存实现 使用redis
securityManager.setCacheManager(cacheManager());
// 自定义session管理 使用redis
securityManager.setSessionManager(sessionManager());
return securityManager;
}

7.整合redis接管session和缓存

  1. 继承FormAuthenticationFilter 重写一些方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
package io.kid19999.backstage.config.Shiro;

import io.kid19999.backstage.repository.SignLogRepository;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.web.filter.authc.FormAuthenticationFilter;
import org.apache.shiro.web.util.WebUtils;
import org.springframework.beans.factory.annotation.Autowired;

import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;

/**
* @desc: 自定义 登录验证 的操作
* @auther: kid1999
* @date: 2019/11/28 20:52
**/

public class AuthenticationFilter extends FormAuthenticationFilter {

@Autowired
private SignLogRepository signLogRepository;

@Override
protected boolean onLoginSuccess(AuthenticationToken token, Subject subject, ServletRequest request, ServletResponse response) throws Exception {
//获取已登录的用户信息
String username = (String) subject.getPrincipal();
//获取session
HttpServletRequest httpServletRequest = WebUtils.toHttp(request);
HttpSession session = httpServletRequest.getSession();
//把用户信息保存到session
session.setAttribute("activeUser", username);

System.out.println("login success");
System.out.println(token.getPrincipal());
String name = token.getPrincipal().toString();
System.out.println(name);

return super.onLoginSuccess(token, subject, request, response);
}


@Override
protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
System.out.println("拒绝访问");
return super.onAccessDenied(request, response);
}

@Override
protected boolean onLoginFailure(AuthenticationToken token, AuthenticationException e, ServletRequest request, ServletResponse response) {
System.out.println("登录失败");
return super.onLoginFailure(token, e, request, response);
}
}

  1. 在ShiroConfig 注入 自定义拦截器
1
2
3
4
5
// 注入 拦截器和认证
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterMap);
Map<String,Filter> filter = new HashMap<>();
filter.put("authc",new AuthenticationFilter());
shiroFilterFactoryBean.setFilters(filter);
  1. 拦截器的实际工作流程:
    详见

8.Vue的简单安全认证:

  1. 导入vue-cookies 使用cookie保存当前用户的状态

    1
    2
    import VueCookies from 'vue-cookies'
    Vue.use(VueCookies);
  2. 使用钩子函数对路由进行权限跳转

1
2
3
4
5
6
7
8
9
10
11
12
13
router.beforeEach((to, from, next) => {
let cookieRoles = window.$cookies.get("role");
if(to.path === '/'){
next();
} else if (!cookieRoles && to.path !== '/login') { // cookie中有登陆用户信息跳转页面,否则到登陆页面
next('/login');
} else if (to.meta.permission) {// 如果该页面配置了权限属性(自定义permission)
// 如果是管理员权限则可进入
roles.indexOf('admin') > -1 ? next() : next('/403');
} else {
next();
}
});
  1. 登录后返回状态 存入cookie中 默认保存一天
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
this.$axios.post('/login', qs.stringify(data), {headers:{'Content-Type':'application/x-www-form-urlencoded'}}).then(function (response) {
let res = response.data;
if(res['reslut'] === 1){
self.$store.commit('Login');
let arr = res['info'].split(' ');
window.$cookies.set("role",arr[1]);
window.$cookies.set("sessionId",arr[0]);
self.$router.push("/");
self.$message.success({message:"登录成功"});
}else{
self.$message.error(res['info']);
}
}).catch(function (error) {
console.log(error);
});
},

流程总结:

  1. 首先 ShiroFilterFactoryBean 查询拦截规则 跳转 login(或者自己访问login)
  2. 登录验证Controller 调用 subject.login(token)
  3. Realm接收信息,并且和数据源的数据进行对比 返回 判断结果
  4. 如果不成功 返回错误,如果成功 去往资源网站。
  5. ShiroFilterFactoryBean 判断资源访问权限
  6. 调用 Realm的 授权方法 从数据源获取 用户权限 返回结果
  7. 如果 不成功 返回错误。成功 允许访问

思考:


前端验证cookie是可见的 role 容易被查看 修改不安全

后端再次验证 session是否存在(超时被抹除|logout被抹除)

每次访问的时候先去过滤器一下是否session存在


参考资料:

知乎-Shiro详解

掘金-spring整合shiro

B站视频

  • Apache Shiro 是ASF旗下的一款开源软件(Shiro发音为“shee-roh”,日语“堡垒(Castle)”的意思),提供了一个强大而灵活的安全框架。可为任何应用提供安全保障— 从命令行应用、移动应用到大型网络及企业应用。

  • Apache Shiro提供了认证、授权、加密和会话管理功能,将复杂的问题隐藏起来,提供清晰直观的API使开发者可以很轻松地开发自己的程序安全代码。并且在实现此目标时无须依赖第三方的框架、容器或服务,当然也能做到与这些环境的整合,使其在任何环境下都可拿来使用。

Shiro的核心四部分

  • 认证(Authentication):用户身份识别。有时可看作为“登录(login)”,它是用户证明自己是谁的一个行为。

  • 授权(Authorization):访问控制过程,好比决定“认证(who)”可以访问“什么(what)”.

  • 会话管理(SessionManagement):管理用户的会话(sessions),甚至在没有WEB或EJB容器的环境中。管理用户与时间相关的状态。

  • 加密(Cryptography):使用加密算法保护数据更加安全,防止数据被偷窥。

1.Spring整合Shiro

  1. pom 依赖
    1
    2
    3
    4
    5
    6
    <!--整合shiro-->
    <dependency>
    <groupId>org.apache.shiro</groupId>
    <artifactId>shiro-spring-boot-starter</artifactId>
    <version>1.4.1</version>
    </dependency>

2. ShiroConfig

  • shiro的基本配置
  • 1.ShiroFilterFactoryBean #Shiro对外的接口,代表当前“用户”
  • 2.DefaultWebSecurityManager #关联realm
  • 3.UserRealm #负责登录验证和资源授权
  • 1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    package io.kid1999.operatesystem.config;

    import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
    import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
    import org.springframework.beans.factory.annotation.Qualifier;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;

    import java.util.LinkedHashMap;
    import java.util.Map;

    /**
    * @author kid1999
    * @title: Shiro 的 配置
    * @date 2019/11/26 21:27
    */

    @Configuration
    public class ShiroConfig {

    // 3. 创建 ShiroFilterFactoryBean
    @Bean
    public ShiroFilterFactoryBean getShiroFilterFactoryBean(@Qualifier("securityManager") DefaultWebSecurityManager securityManager){
    ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
    // 设置安全管理器
    shiroFilterFactoryBean.setSecurityManager(securityManager);

    /** 添加Shiro的内置过滤器
    * 常用:
    * anon : 无需认证(登录) 可以访问
    * authc :必须认证才可以访问
    * user : 如果使用remenberMe功能可以直接访问
    * perms: 该资源必须获得相关权限才可访问
    * role: 该资源必须获得角色权限才可以访问
    *
    * 注意: 这个授权拦截链 是按顺序执行的!!!
    */
    Map<String,String> filterMap = new LinkedHashMap<>();
    filterMap.put("/","anon");
    filterMap.put("/login","anon");
    filterMap.put("/error","anon");
    // filterMap.put("/admin","authc");

    // 权限过滤器 也可以设置角色过滤
    filterMap.put("/admin","perms[user:admin]");

    shiroFilterFactoryBean.setLoginUrl("/login"); // 设置跳转的登录页面
    shiroFilterFactoryBean.setUnauthorizedUrl("/noAuth"); // 设置未授权访问页面
    shiroFilterFactoryBean.setFilterChainDefinitionMap(filterMap);

    return shiroFilterFactoryBean;
    }

    // 2. 创建DefaultWebSecurityManager
    // 所有与安全有关的操作都会与SecurityManager进行交互
    @Bean(name = "securityManager")
    public DefaultWebSecurityManager getDefaultWebSecurityManager(@Qualifier("getRealm") UserRealm userRealm){
    DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
    // 关联realm
    securityManager.setRealm(getRealm());
    return securityManager;
    }

    // 1. 创建Realm
    // Shiro从Realm获取安全数据(用户、角色、权限)
    @Bean
    public UserRealm getRealm(){
    return new UserRealm();
    }
    }

3. UserRealm

  • Shiro从从Realm获取安全数据(如用户、角色、权限)
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    package io.kid1999.operatesystem.config;

    import io.kid1999.operatesystem.model.Admin;
    import io.kid1999.operatesystem.repository.AdminRepository;
    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;

    /**
    * @author kid1999
    * @title: UserRealm 自定义 realm 处理 授权和认证
    * @date 2019/11/26 21:29
    */



    public class UserRealm extends AuthorizingRealm {
    @Autowired
    private AdminRepository adminRepository;

    // 授权执行逻辑
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
    System.out.println("授权执行逻辑");

    // 给资源进行授权
    SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();

    // 添加资源授权字符串
    info.addStringPermission("user:admin");

    return info;
    }

    // 认证执行逻辑
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
    System.out.println("认证执行逻辑");

    // 1. 获取传来验证的token
    UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken;
    String name = token.getUsername();
    String password = String.valueOf(token.getPassword());
    // 2. 通过用户名 取出数据库的数据
    Admin user = adminRepository.findByAdminId(name);

    // 3. 判断用户名
    if (user == null){
    return null; // 找不到用户名 报 UnknownAccountException
    }

    // 4. 判断密码
    /**
    * arg0: 回传login的数据
    * arg1: 数据库的密码
    * arg2: realm 的name
    */
    return new SimpleAuthenticationInfo("",user.getAdminPwd(),"");
    }
    }

4.AdminRepository 链接数据库

  • Realm 会连接数据源 进行身份验证
  • 此处放上我的JPA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package io.kid1999.operatesystem.repository;
import io.kid1999.operatesystem.model.Admin;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;

/**
* @author kid1999
* @title: ManagerRepository
* @date 2019/11/24 10:07
*/
public interface AdminRepository extends JpaRepository<Admin,Integer> {
@Query(value = "select * from admin m where m.admin_id = ?1 and m.admin_pwd = ?2" ,nativeQuery = true)
Admin login(String studentId, String passWord);

Admin findByAdminId(String adminId);
}

5.登录Controller

  • 此处没有使用加密 和 缓存机制
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    /**
    * 使用Shiro实现登录
    */
    @PostMapping("/login")
    String toLogin(String studentId,
    String passWord,
    Model model){
    // 1.获取Subject
    Subject subject = SecurityUtils.getSubject();
    // 2.封装登录方法
    UsernamePasswordToken token = new UsernamePasswordToken(studentId,passWord);
    // 3.执行登录方法
    try {
    subject.login(token); // 调用login方法 -> realm 做验证
    return "admin";
    }catch (UnknownAccountException e){
    model.addAttribute("msg","用户名不存在");
    return "login";
    }catch (IncorrectCredentialsException e){
    model.addAttribute("msg","密码错误");
    return "login";
    }
    }

流程总结:

  1. 首先 ShiroFilterFactoryBean 查询拦截规则 跳转 login(或者自己访问login)
  2. 登录验证Controller 调用 subject.login(token)
  3. Realm接收信息,并且和数据源的数据进行对比 返回 判断结果
  4. 如果不成功 返回错误,如果成功 去往资源网站。
  5. ShiroFilterFactoryBean 判断资源访问权限
  6. 调用 Realm的 授权方法 从数据源获取 用户权限 返回结果
  7. 如果 不成功 返回错误。成功 允许访问

参考资料:

知乎-Shiro详解

B站视频