Shiro

Shiro

参考Shiro安全框架【快速入门】就这一篇! - 知乎 (zhihu.com)

做什么的

开源安全框架,处理身份验证、授权、加密和会话管理

  • 认证:识别用户身份(登录)
  • 授权:给用户某些操作的权限(赋予角色)
  • 加密:对数据源使用加密算法
  • 会话管理:特定于用户的会话管理

整体架构

分为Subject,SecurityManager和 Realm三层

核心组件

  • Subject
1
2
3
可以是一个通过浏览器请求的用户,也可能是一个运行的程序

Subject在shiro中是一个接口,外部程序通过subject进行认证授,而subject是通过SecurityManager安全管理器进行认证授权
  • SecurityManager
1
2
3
4
5
shiro的核心
对所有的subject进行安全管理,可以完成subject的认证、授权等
通过Authenticator认证器进行认证
通过Authorizer授权器进行授权
通过SessionManager进行会话管理
  • Realm
1
2
数据库读取+认证功能+授权功能实现
相当于datasource数据源,securityManager进行安全认证需要通过Realm获取用户权限数据
  • CacheManager
1
将用户权限数据存储在缓存以提高性能
  • Cryptography
1
密码管理

入门案例

身份认证

从ini文件读取
  1. 导入依赖

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    <dependency>
    <groupId>commons-logging</groupId>
    <artifactId>commons-logging</artifactId>
    <version>1.1.3</version>
    </dependency>

    <dependency>
    <groupId>org.apache.shiro</groupId>
    <artifactId>shiro-core</artifactId>
    <version>1.3.2</version>
    </dependency>

    <dependency>
    <groupId>junit</groupId>
    <artifactId>junit</artifactId>
    <version>4.11</version>
    </dependency>
  2. 在resource目录下编写ini文件

    1
    2
    3
    #声明用户账号
    [users]
    jay=123
  3. 编写测试类

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    public class HelloShiro {

    @Test
    public void shiroLogin() {
    // 导入权限ini文件构建权限工厂
    IniSecurityManagerFactory factory = new IniSecurityManagerFactory("classpath:shiro.ini");
    // 用工厂构建Manager
    SecurityManager securityManager = factory.getInstance();
    // 使Manager生效
    SecurityUtils.setSecurityManager(securityManager);

    // 获取Subject
    Subject subject = SecurityUtils.getSubject();
    // 创建Token
    UsernamePasswordToken token = new UsernamePasswordToken("jay", "123");
    // subject通过token登录
    subject.login(token);
    System.out.println("是否登录成功:" + subject.isAuthenticated());
    }
    }
从Realm中读取
  1. 编写service,模拟从数据库读取用户密码

    1
    2
    3
    4
    5
    6
    7
    public class SecurityServiceImpl implements SecurityService {
    @Override
    public String findPasswordByLoginName(String loginName) {
    // 模拟从数据库中获取密码
    return "123";
    }
    }
  2. 自定义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
    public class DefinitionRealm extends AuthorizingRealm {
    SecurityService securityService = new SecurityServiceImpl();

    /**
    * 授权
    * @param principalCollection
    * @return
    */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
    return null;
    }

    /**
    * 认证
    * @param authenticationToken
    * @return
    * @throws AuthenticationException
    */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
    // 获取用户唯一标识
    String loginName = (String) authenticationToken.getPrincipal();
    String password = securityService.findPasswordByLoginName(loginName);
    if (password == null || "".equals(password))
    throw new UnknownAccountException("账户不存在");
    // 传递账号和密码
    return new SimpleAuthenticationInfo(loginName, password, this.getName());
    }
    }
  3. 在ini文件中设置realm

    1
    2
    3
    [main]
    definitionRealm=com.xw.realm.DefinitionRealm
    securityManager.realms=$definitionRealm
  4. 编写测试类

    和上面的一样

  5. 认证过程

    1
    2
    Subject -> SecurityManager -> Authenticator -> Realm -> 自定义Realm将数据库中查到的账号密码返回
    最后在Realm中比较Subject传过来的Token和数据库中的是否一致

编码解码

通过shiro的Hex和Base64来进行编码解码

  1. 编写工具类(把两种方式整合到一个工具类上)

    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
    public class EncodesUtils {
    /**
    * 按照16进制编码
    * @param bytes
    * @return
    */
    public static String encodeHex(byte[] bytes) {
    return Hex.encodeToString(bytes);
    }

    /**
    * 按照16进制解码
    * @param s
    * @return
    */
    public static byte[] decodeHex(String s) {
    return Hex.decode(s);
    }

    /**
    * 按照16进制编码
    * @param bytes
    * @return
    */
    public static String encodeBase64(byte[] bytes) {
    return Base64.encodeToString(bytes);
    }

    /**
    * 按照16进制解码
    * @param s
    * @return
    */
    public static byte[] decodeBase64(String s) {
    return Base64.decode(s);
    }
    }
  2. 测试

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    @Test
    public void test1() {
    String pwd = "123456";
    // 测试16进制编码解码
    String encode = EncodesUtils.encodeHex(pwd.getBytes());
    String decode = new String(EncodesUtils.decodeHex(encode));
    System.out.println(decode.equals(pwd) ? "相等" : "不相等");
    // 测试base64编码解码
    encode = EncodesUtils.encodeBase64(pwd.getBytes());
    decode = new String(EncodesUtils.decodeBase64(encode));
    System.out.println(decode.equals(pwd) ? "相等" : "不相等");
    }

散列算法

通过SimpleHash(算法名称,原密码,盐值,加密次数)来进行加密

  1. 编写工具类

    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
    public class DigestUtil {

    // 算法名称
    private static final String SHA1 = "SHA-1";

    // 加密次数
    private static final Integer ITERATIONS = 512;

    /**
    * 通过sha1加密
    * @param s
    * @param salt
    * @return
    */
    public static String sha1(String s, String salt) {
    return new SimpleHash(SHA1, s, salt, ITERATIONS).toString();
    }

    /**
    * 返回加密后的密码和盐值
    * @param passwordPlain
    * @return
    */
    public static Map<String, String> entryptPassword(String passwordPlain) {
    HashMap<String, String> map = new HashMap<>();
    String salt = generateSalt();
    map.put("salt", salt);
    map.put("password", sha1(passwordPlain, salt));
    return map;
    }

    /**
    * 生成随机盐值
    * @return
    */
    public static String generateSalt() {
    SecureRandomNumberGenerator randomNumberGenerator = new SecureRandomNumberGenerator();
    return randomNumberGenerator.nextBytes().toHex();
    }
    }

  2. 测试

    1
    2
    3
    4
    5
    6
    @Test
    public void test2() {
    String pwd = "123456";
    Map<String, String> map = DigestUtil.entryptPassword(pwd);
    System.out.println(map.toString());
    }

Realm中指定匹配器

自定义匹配器的盐值,加密次数

  1. service层返回加密后的密码和盐值

    1
    2
    3
    4
    5
    6
    7
    public class SecurityServiceImpl implements SecurityService {
    @Override
    public Map<String, String> findPasswordByLoginName(String loginName) {
    // 模拟从数据库中获取密码
    return DigestUtil.entryptPassword("123");
    }
    }
  2. 自定义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
    public class DefinitionRealm extends AuthorizingRealm {
    SecurityService securityService = new SecurityServiceImpl();

    public DefinitionRealm() {
    // 创建匹配器,指定匹配方式
    HashedCredentialsMatcher matcher = new HashedCredentialsMatcher(DigestUtil.SHA1);
    // 指定迭代次数
    matcher.setHashIterations(DigestUtil.ITERATIONS);
    // 使匹配器生效
    setCredentialsMatcher(matcher);
    }

    /**
    * 授权
    * @param principalCollection
    * @return
    */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
    return null;
    }

    /**
    * 认证
    * @param authenticationToken
    * @return
    * @throws AuthenticationException
    */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
    // 获取用户唯一标识
    String loginName = (String) authenticationToken.getPrincipal();
    Map<String, String> map = securityService.findPasswordByLoginName(loginName);
    if (map.isEmpty())
    throw new UnknownAccountException("账户不存在");
    String password = map.get("password");
    String salt = map.get("salt");

    return new SimpleAuthenticationInfo(loginName, password, ByteSource.Util.bytes(salt), this.getName());
    }
    }
  3. 测试

身份授权

  1. service层返回查询到的角色、权限信息

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    @Override
    public List<String> findRoleByLoginName(String loginName) {
    // 模拟从数据库中获取角色
    List<String> list = new ArrayList<>();
    list.add("admin");
    list.add("dev");
    return list;
    }

    @Override
    public List<String> findPermissionByLoginName(String loginName) {
    // 模拟从数据库中获取权限
    List<String> list = new ArrayList<>();
    list.add("order:add");
    list.add("order:list");
    list.add("order:del");
    return list;
    }
  2. 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
    public class DefinitionRealm extends AuthorizingRealm {
    SecurityService securityService = new SecurityServiceImpl();

    /**
    * 授权
    * @param principalCollection
    * @return
    */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
    // 获取用户唯一标识
    String loginName = (String) principalCollection.getPrimaryPrincipal();
    // 从数据库中获取用户角色和权限
    List<String> roles = securityService.findRoleByLoginName(loginName);
    List<String> permissions = securityService.findPermissionByLoginName(loginName);
    // 构建校验信息
    SimpleAuthorizationInfo authenticationInfo = new SimpleAuthorizationInfo();
    authenticationInfo.addRoles(roles);
    authenticationInfo.addStringPermissions(permissions);

    return authenticationInfo;
    }
    }

  3. 测试

    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
    @Test
    public void testPermissionRealm() {
    Subject subject = shiroLogin();

    // 验证角色
    System.out.println("是否有admin角色:" + subject.hasRole("admin")); // true
    System.out.println("是否有coder角色:" + subject.hasRole("coder")); // false

    // 验证权限
    System.out.println("是否有添加订单权限:" + subject.isPermitted("order:add")); // true
    System.out.println("是否有修改订单权限:" + subject.isPermitted("order:update")); // false

    }

    public Subject shiroLogin() {
    // 导入权限ini文件构建权限工厂
    IniSecurityManagerFactory factory = new IniSecurityManagerFactory("classpath:shiro.ini");
    // 用工厂构建Manager
    SecurityManager securityManager = factory.getInstance();
    // 使Manager生效
    SecurityUtils.setSecurityManager(securityManager);

    // 获取subject
    Subject subject = SecurityUtils.getSubject();
    // 创建Token
    UsernamePasswordToken token = new UsernamePasswordToken("jay", "123");
    // subject通过token登录
    subject.login(token);
    System.out.println("是否登录成功:" + subject.isAuthenticated());

    return subject;
    }

使用案例

Shiro认证过程

验证用户身份

测试类

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
public class AuthenticationTest {

SimpleAccountRealm simpleAccountRealm = new SimpleAccountRealm();

@Before
public void addUser() {
// 往Realm域中添加用户信息
simpleAccountRealm.addAccount("user", "123456");
}

@Test
public void testAuthentication() {
// 1.构建SecurityManager环境
DefaultSecurityManager defaultSecurityManager = new DefaultSecurityManager();
defaultSecurityManager.setRealm(simpleAccountRealm);

SecurityUtils.setSecurityManager(defaultSecurityManager); // 设置SecurityManager环境
Subject subject = SecurityUtils.getSubject(); // 获取当前主体

// 2.主体提交认证请求
UsernamePasswordToken token = new UsernamePasswordToken("user", "123456");
subject.login(token); // 登录

System.out.println("isAuthenticated:" + subject.isAuthenticated());

subject.logout();

System.out.println("isAuthenticated:" + subject.isAuthenticated());
}
}

Shiro授权过程

赋予/验证用户角色

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
public class AuthenticationTest2 {

SimpleAccountRealm simpleAccountRealm = new SimpleAccountRealm();

@Before
public void addUser() {
// 往Realm域中添加用户信息
// 使其具备admin和user两个角色
simpleAccountRealm.addAccount("user", "123456", "admin", "user");
}

@Test
public void testAuthentication() {
// 1.构建SecurityManager环境
DefaultSecurityManager defaultSecurityManager = new DefaultSecurityManager();
defaultSecurityManager.setRealm(simpleAccountRealm);

SecurityUtils.setSecurityManager(defaultSecurityManager); // 设置SecurityManager环境
Subject subject = SecurityUtils.getSubject(); // 获取当前主体

// 2.主体提交认证请求
UsernamePasswordToken token = new UsernamePasswordToken("user", "123456");
subject.login(token); // 登录

System.out.println("isAuthenticated:" + subject.isAuthenticated());

// 验证用户是否有相应的角色,没有则报错
subject.checkRoles("admin", "user");

// subject.checkRoles("admin", "user", "xxx"); // UnauthorizedException

}
}

自定义Realm

realm类似MVC的repository层

自定义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
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
public class MyRealm extends AuthorizingRealm {

/**
* 模拟数据库数据
*/
Map<String, String> userMap = new HashMap<>(16);

{
userMap.put("user", "123456");
super.setName("myRealm"); // 设置自定义Realm的名称
}

/**
* 授权
* @param principalCollection
* @return
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
String userName = (String) principalCollection.getPrimaryPrincipal();

// 从数据库中获取角色和权限数据
Set<String> roles = getRolesByUserName(userName);
Set<String> permissions = getPermissionsByUserName(userName);

// 添加到授权信息中并返回
SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
simpleAuthorizationInfo.setStringPermissions(permissions);
simpleAuthorizationInfo.setRoles(roles);

return simpleAuthorizationInfo;
}

/**
* 认证
* @param authenticationToken
* @return
* @throws AuthenticationException
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
String username = (String) authenticationToken.getPrincipal();

String password = getPasswordByUserName(username);
if (password == null) {
return null;
}

SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo("user", password, "myRealm");

return simpleAuthenticationInfo;
}


/**
* 根据用户名获取权限信息
* @return
*/
private Set<String> getPermissionsByUserName(String username) {
// 模拟查询数据库并返回
HashSet<String> permission = new HashSet<>();
permission.add("user:delete");
permission.add("user:add");
return permission;
}

/**
* 获取角色数据
*
* @param userName
* @return
*/
private Set<String> getRolesByUserName(String userName) {
// 模拟查询数据库并返回
Set<String> roles = new HashSet<>();
roles.add("admin");
roles.add("user");
return roles;
}

/**
* 获取用户密码
* @param userName
* @return
*/
private String getPasswordByUserName(String userName) {
// 模拟获取用户密码/凭证
return userMap.get(userName);
}
}

测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class AuthenticationTest3 {

MyRealm myRealm = new MyRealm();

@Test
public void testAuthentication() {
// 1.构建SecurityManager环境
DefaultSecurityManager defaultSecurityManager = new DefaultSecurityManager();
defaultSecurityManager.setRealm(myRealm);

SecurityUtils.setSecurityManager(defaultSecurityManager);
Subject subject = SecurityUtils.getSubject(); // 获取当前主体

// 2.主体提交认证请求
UsernamePasswordToken token = new UsernamePasswordToken("user", "123456");
subject.login(token); // 登录

subject.checkRoles("admin", "user");

subject.checkPermission("user:add");
}
}

Shiro加密

使用MD5加密可以使数据库中的密码不是明文保存,数据库泄露的损失会减小

但可以通过用一些简单常用的密码来撞库,从而反推原密码

解决方式:

加盐:在原始密码上加上随机数,再进行MD5加密。需要把随机数也存到数据库中,以便之后进行验证

多次加密:多次加密MD5,从而让攻击者不知道加密的次数

Shiro实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class AuthenticationTest4 {

@Test
public void testAuthentication() {
String password = "123456";

// 盐值(随机数)
String salt = new SecureRandomNumberGenerator().nextBytes().toString();
// 加密次数
int times = 2;
// 加密方式
String alogrithmName = "md5";

SimpleHash encodePassword = new SimpleHash(alogrithmName, password, salt, times);
System.out.println("原密码:" + password);
System.out.println("加密后:" + encodePassword);
}
}

小dmeo

SpringBoot集成Shiro

SpringBoot之整合Shiro springboot+shiro

整合Shiro

导入依赖
1
2
3
4
5
6
<!--shiro-->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring-boot-starter</artifactId>
<version>1.5.3</version>
</dependency>
编写自定义Realm
1
2
3
4
5
6
7
8
9
10
11
12
13
package com.xw.shiro.realm;

public class CustomerRealm extends AuthorizingRealm {
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
return null;
}

@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
return null;
}
}
编写Shiro配置类

主要配置过滤器、SecurityManager、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
package com.xw.config;

import java.util.HashMap;
import java.util.Map;

@Configuration
public class ShiroConfig {

// 过滤器
@Bean
public ShiroFilterFactoryBean getShiroFilterFactoryBean(DefaultWebSecurityManager securityManager) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
// 给ShiroFilter配置安全管理器
shiroFilterFactoryBean.setSecurityManager(securityManager);
//配置系统受限资源
//配置系统公共资源
Map<String, String> map = new HashMap<String, String>();
map.put("/index.jsp","authc");//表示这个资源需要认证和授权
// 设置认证界面路径
shiroFilterFactoryBean.setLoginUrl("/login.jsp");
shiroFilterFactoryBean.setFilterChainDefinitionMap(map);

return shiroFilterFactoryBean;
}

// SecurityManager
@Bean
public DefaultWebSecurityManager getDefaultWebSecurityManager(Realm realm) {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(realm);
return securityManager;
}

// 自定义Realm
@Bean
public Realm getRealm() {
CustomerRealm customerRealm = new CustomerRealm();
return customerRealm;
}
}

登录登出

编写Controller

登录登出方法

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
package com.xw.controller;

@Controller
@RequestMapping("/user")
public class UserController {
@RequestMapping("/logout")
public String logout() {
// 获取主体
Subject subject = SecurityUtils.getSubject();
// 退出
subject.logout();

// 重定向到登录界面
return "redirect:/login.jsp";
}

@RequestMapping("/login")
public String login(String username, String password) {
Subject subject = SecurityUtils.getSubject();
UsernamePasswordToken token = new UsernamePasswordToken(username, password);

try {
// 登录
subject.login(token);
System.out.println("登录成功");
return "redirect:/index.jsp";
} catch (UnknownAccountException e) {
e.printStackTrace();
System.out.println("用户错误");
} catch (IncorrectCredentialsException e) {
System.out.println("密码错误");
}
return "redirect:/login.jsp";
}
}
编写自定义Realm的认证方法
1
2
3
4
5
6
7
8
9
10
11
12
// 认证
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
String username = (String) authenticationToken.getPrincipal();
// 模拟从数据库中获取账号密码
String account = "zhangsan";
String password = "123456";
if (account.equals(username)) {
return new SimpleAuthenticationInfo(username, password, this.getName());
}
return null;
}
修改配置类

将login方法设为公共资源,其他的为受限资源

1
2
3
Map<String, String> map = new HashMap<String, String>();
map.put("/user/login","anon");//表示这个为公共资源 一定是在受限资源上面
map.put("/**","authc");//表示这个受限资源需要认证和授权

MD5、Salt认证

编写生成盐值工具类
1
2
3
4
5
6
7
package com.xw.utils;

public class SaltUtil {
public static String getSalt() {
return new SecureRandomNumberGenerator().nextBytes().toString();
}
}
编写获取对象工具类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package com.xw.utils;

@Component
public class ApplicationContextUtil implements ApplicationContextAware {
private static ApplicationContext context;

@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.context = applicationContext;
}

public static Object getBean(String beanName) {
return context.getBean(beanName);
}
}
编写Service注册方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package com.xw.service.impl;

@Service("userService")
public class UserServiceImpl implements UserService {
@Autowired
private UserMapper userMapper;

@Override
public void register(User user) {
// 添加盐值
String salt = SaltUtil.getSalt();
user.setSalt(salt);
// 明文密码进行md5 + salt + hash散列
Md5Hash MD5 = new Md5Hash(user.getPassword(), salt, 1024);
user.setPassword(MD5.toHex());
userMapper.save(user);
}

@Override
public User findByUsername(String username) {
return userMapper.findByUsername(username);
}
}

自定义Realm中改为从Service中获取数据
1
2
3
4
5
6
7
8
9
10
11
12
// 认证
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
String username = (String) authenticationToken.getPrincipal();
// 获取userService对象
UserService userService = (UserService) ApplicationContextUtil.getBean("userService");
User user = userService.findByUsername(username);
if (!ObjectUtils.isEmpty(user)) {
return new SimpleAuthenticationInfo(user.getUsername(), user.getPassword(), ByteSource.Util.bytes(user.getSalt()), this.getName());
}
return null;
}
修改配置类

修改公共资源和添加匹配器

1
2
3
4
map.put("/user/login","anon");//表示这个为公共资源 一定是在受限资源上面
map.put("/user/register","anon");//表示这个为公共资源 一定是在受限资源上面
map.put("/register.jsp","anon");//表示这个为公共资源 一定是在受限资源上面
map.put("/**","authc");//表示这个受限资源需要认证和授权
1
2
3
4
5
6
7
8
9
10
11
12
// 自定义Realm
@Bean
public Realm getRealm() {
// 匹配器
HashedCredentialsMatcher credentialsMatcher = new HashedCredentialsMatcher();
credentialsMatcher.setHashAlgorithmName("md5");
credentialsMatcher.setHashIterations(1024);
// 添加匹配器
CustomerRealm customerRealm = new CustomerRealm();
customerRealm.setCredentialsMatcher(credentialsMatcher);
return customerRealm;
}

授权

授予角色和权限

User <–> Role <–> Perm

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
// 授权
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
// 获取用户唯一标识
String principal = (String) principalCollection.getPrimaryPrincipal();
// 获取UserService
UserService userService = (UserService) ApplicationContextUtil.getBean("userService");

User user = userService.findRoleByUsername(principal);
if (!CollectionUtils.isEmpty(user.getRoles())) {
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
user.getRoles().forEach(role -> {
// 将用户角色放到info中并返回
info.addRole(role.getName());
// 根据角色id获取角色对应的权限信息
List<Perms> perms = userService.findPermsByRoleId2(role.getId());
if (!CollectionUtils.isEmpty(perms) && perms.get(0) != null) {
perms.forEach(perm -> info.addStringPermission(perm.getName()));
}
});
return info;
}

return null;
}
判断是否有角色和权限
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
package com.xw.controller;

@Controller
@RequestMapping("order")
public class OrderController {
// 基于方法
// @RequestMapping("save")
// public String save() {
// //基于角色
// //获取主体对象
// Subject subject = SecurityUtils.getSubject();
// //代码方式
// if (subject.hasRole("admin")) {
// System.out.println("保存订单!");
// }else{
// System.out.println("无权访问!");
// }
// System.out.println("进入save方法============");
// return "redircet:/index.jsp";
// }

// 基于注解
@RequiresRoles(value={"admin","user"})//用来判断角色 同时具有 admin user
@RequiresPermissions("user:update:01") //用来判断权限字符串
@RequestMapping("save")
public String save(){
System.out.println("进入方法");
return "redirect:/index.jsp";
}
}

启用缓存

将用户的角色和权限保存在缓存中,不用每次都去数据库查

EhCache

EhCache是shiro的默认缓存

导入依赖

1
2
3
4
5
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-ehcache</artifactId>
<version>1.5.3</version>
</dependency>

设置缓存管理器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Bean
public Realm getRealm() {
// 匹配器
HashedCredentialsMatcher credentialsMatcher = new HashedCredentialsMatcher();
credentialsMatcher.setHashAlgorithmName("md5");
credentialsMatcher.setHashIterations(1024);
// 添加匹配器
CustomerRealm customerRealm = new CustomerRealm();
customerRealm.setCredentialsMatcher(credentialsMatcher);

// 开启缓存管理器
customerRealm.setCacheManager(new EhCacheManager());
customerRealm.setCachingEnabled(true);//开启缓存
customerRealm.setAuthenticationCachingEnabled(true);//开启认证缓存
customerRealm.setAuthenticationCacheName("authentication");
customerRealm.setAuthorizationCachingEnabled(true);//开启授权缓存
customerRealm.setAuthorizationCacheName("authorization");
return customerRealm;
}
Redis

导入依赖

1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

配置Redis

1
2
3
4
5
spring:
redis:
port: 6379
host: localhost
database: 0

编写Redis缓存管理器

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 com.xw.config.redis;

import com.xw.utils.ApplicationContextUtil;
import org.apache.shiro.cache.Cache;
import org.apache.shiro.cache.CacheException;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.StringRedisSerializer;

import java.util.Collection;
import java.util.Set;

//自定义redis缓存的实现
public class RedisCache<k,v> implements Cache<k,v> {

private String cacheName;

public RedisCache() {
}

public RedisCache(String cacheName) {
this.cacheName = cacheName;
}

@Override
public v get(k k) throws CacheException {
return (v) getRedisTemplate().opsForHash().get(this.cacheName, k.toString());
}

@Override
public v put(k k, v v) throws CacheException {
System.out.println("put key: "+k);
System.out.println("put value:"+v);
getRedisTemplate().opsForHash().put(this.cacheName,k.toString(), v);
return null;
}

@Override
public v remove(k k) throws CacheException {
System.out.println("=============remove=============");
return (v) getRedisTemplate().opsForHash().delete(this.cacheName,k.toString());
}

@Override
public void clear() throws CacheException {
System.out.println("=============clear==============");
getRedisTemplate().delete(this.cacheName);
}

@Override
public int size() {
return getRedisTemplate().opsForHash().size(this.cacheName).intValue();
}

@Override
public Set<k> keys() {
return getRedisTemplate().opsForHash().keys(this.cacheName);
}

@Override
public Collection<v> values() {
return getRedisTemplate().opsForHash().values(this.cacheName);
}

private RedisTemplate getRedisTemplate(){
RedisTemplate redisTemplate = (RedisTemplate) ApplicationContextUtil.getBean("redisTemplate");
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setHashKeySerializer(new StringRedisSerializer());
return redisTemplate;
}
}
1
2
3
4
5
6
7
8
9
package com.xw.config.redis;

public class RedisCacheManager implements CacheManager {
@Override
public <K, V> Cache<K, V> getCache(String cacheName) throws CacheException {
System.out.println(cacheName);
return new RedisCache<K, V>(cacheName);
}
}

开启缓存管理器的地方改成RedisCacheManager

simpleByteSource实现没有实现序列化,如果使用到了盐值,需要自定义盐值

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
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
package com.xw.shiro.salt;

//自定义salt实现 实现序列化接口
public class MyByteSource implements ByteSource, Serializable {
private byte[] bytes;
private String cachedHex;
private String cachedBase64;

public MyByteSource(byte[] bytes) {
this.bytes = bytes;
}

public MyByteSource(char[] chars) {
this.bytes = CodecSupport.toBytes(chars);
}

public MyByteSource(String string) {
this.bytes = CodecSupport.toBytes(string);
}

public MyByteSource(ByteSource source) {
this.bytes = source.getBytes();
}

public MyByteSource(File file) {
this.bytes = (new MyByteSource.BytesHelper()).getBytes(file);
}

public MyByteSource(InputStream stream) {
this.bytes = (new MyByteSource.BytesHelper()).getBytes(stream);
}

public static boolean isCompatible(Object o) {
return o instanceof byte[] || o instanceof char[] || o instanceof String || o instanceof ByteSource || o instanceof File || o instanceof InputStream;
}

public byte[] getBytes() {
return this.bytes;
}

public boolean isEmpty() {
return this.bytes == null || this.bytes.length == 0;
}

public String toHex() {
if (this.cachedHex == null) {
this.cachedHex = Hex.encodeToString(this.getBytes());
}

return this.cachedHex;
}

public String toBase64() {
if (this.cachedBase64 == null) {
this.cachedBase64 = Base64.encodeToString(this.getBytes());
}

return this.cachedBase64;
}

public String toString() {
return this.toBase64();
}

public int hashCode() {
return this.bytes != null && this.bytes.length != 0 ? Arrays.hashCode(this.bytes) : 0;
}

public boolean equals(Object o) {
if (o == this) {
return true;
} else if (o instanceof ByteSource) {
ByteSource bs = (ByteSource)o;
return Arrays.equals(this.getBytes(), bs.getBytes());
} else {
return false;
}
}

private static final class BytesHelper extends CodecSupport {
private BytesHelper() {
}

public byte[] getBytes(File file) {
return this.toBytes(file);
}

public byte[] getBytes(InputStream stream) {
return this.toBytes(stream);
}
}
}

把自定义Realm中认证地方的simpleByteSource改成自定义的MyByteSource


Shiro
http://xwww12.github.io/2022/11/26/其他/Shiro/
作者
xw
发布于
2022年11月26日
许可协议