本案例使用jsp作为前端页面展示形式,所以新建的springboot工程需要进行一下配置
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- 引入jsp依赖 -->
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-jasper</artifactId>
</dependency>
<dependency>
<groupId>jstl</groupId>
<artifactId>jstl</artifactId>
<version>1.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>
</dependencies>
在main
目录下我们新建一个webapp
目录,目录里面新建一个jsp文件index.jsp
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>index</title>
</head>
<body>
hello world!
</body>
</html>
这里webapp没有一个蓝色小圆圈,可以参考:SpringBoot中集成JSP如何创建webapp
spring:
# 设置视图模板为jsp
mvc:
view:
prefix: /
suffix: .jsp
<dependencies>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
<version>1.4.0</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.4.0</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-web</artifactId>
<version>1.4.0</version>
</dependency>
</dependencies>
我们知道实际开发中使用shiro时都是使用自定的realm,我们先不管三七二十一,先自定义一个realm,暂时不实现认证和授权
package com.mye.hl21shrio.shiro.realm;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
/** * 自定义realm */
public class CustomerRealm extends AuthorizingRealm {
// 授权
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
return null;
}
// 认证
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
return null;
}
}
这个类是Shiro的核心配置类,里面继承了ShiroFilter、SecurityManager和上面的自定义的Realm
package com.mye.hl21shrio.config;
import com.mye.hl21shrio.shiro.realm.CustomerRealm;
import org.apache.shiro.realm.Realm;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.HashMap;
import java.util.Map;
/** * Shiro的核心配置类,用来整合shiro框架 */
@Configuration
public class ShiroConfiguration {
//1.创建shiroFilter //负责拦截所有请求
@Bean(name = "shiroFilterFactoryBean")
public ShiroFilterFactoryBean shiroFilterFactoryBean(DefaultWebSecurityManager defaultWebSecurityManager){
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
//给filter设置安全管理器
shiroFilterFactoryBean.setSecurityManager(defaultWebSecurityManager);
//配置系统受限资源
//配置系统公共资源
Map<String,String> map = new HashMap<String,String>();
map.put("/index.jsp","authc");//authc 请求这个资源需要认证和授权
//身份认证失败,则跳转到登录页面的配置 没有登录的用户请求需要登录的页面时自动跳转到登录页面,
// 不是必须的属性,不输入地址的话会自动寻找项目web项目的根目录下的”/login.jsp”页面。
shiroFilterFactoryBean.setLoginUrl("/login.jsp");
shiroFilterFactoryBean.setFilterChainDefinitionMap(map);
return shiroFilterFactoryBean;
}
//2.创建安全管理器
@Bean(name = "defaultWebSecurityManager")
public DefaultWebSecurityManager defaultWebSecurityManager(Realm realm){
DefaultWebSecurityManager defaultWebSecurityManager = new DefaultWebSecurityManager();
//给安全管理器设置
defaultWebSecurityManager.setRealm(realm);
return defaultWebSecurityManager;
}
//3.创建自定义realm
@Bean
public Realm getRealm(){
return new CustomerRealm();
}
}
在上面的ShiroConfiguration
类中,我们看到未认证的用户访问受限资源(这里指index.jsp)时会自动跳转到登录页面login.jsp,shiro默认的登录页面也是这个,我们在webapp下新建一个login.jsp
<%--解决页面乱码--%>
<%@page contentType="text/html; UTF-8" pageEncoding="UTF-8" isELIgnored="false" %>
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>index</title>
</head>
<body>
<h1>用户登录</h1>
</body>
</html>
与login.jsp区别开来,我们更改index.jsp的内容如下
<%--解决页面乱码--%>
<%@page contentType="text/html; UTF-8" pageEncoding="UTF-8" isELIgnored="false" %>
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>login</title>
</head>
<body>
<h1>系统主页</h1>
<ul>
<li><a href="">用户管理</a></li>
<li><a href="">订单管理</a></li>
</ul>
</body>
</html>
这里访问127.0.0.1:8080/index.jsp
跳转到127.0.0.1:8080/login.jsp
则说明shiro配置成功
<form action="${pageContext.request.contextPath}/user/login" method="post">
用户名:<input type="text" name="username" > <br/>
密码 : <input type="text" name="password"> <br>
<input type="submit" value="登录">
</form>
可以看到上面jsp的登录请求是/user/login
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.IncorrectCredentialsException;
import org.apache.shiro.authc.UnknownAccountException;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.subject.Subject;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
@Controller
@RequestMapping("user")
public class UserController {
/** * 用户登录 * @param username * @param password * @return */
@RequestMapping("login")
public String login(String username,String password){
// 获取当前登录用户
Subject subject = SecurityUtils.getSubject();
try {
// 执行登录操作
subject.login(new UsernamePasswordToken(username,password));
// 认证通过后直接跳转到index.jsp
return "redirect:/index.jsp";
} catch (UnknownAccountException e) {
e.printStackTrace();
System.out.println("用户名错误~");
} catch (IncorrectCredentialsException e) {
e.printStackTrace();
System.out.println("密码错误~");
} catch (Exception e) {
e.printStackTrace();
}
// 如果认证失败仍然回到登录页面
return "redirect:/login.jsp";
}
}
<%--解决页面乱码--%>
<%@page contentType="text/html; UTF-8" pageEncoding="UTF-8" isELIgnored="false" %>
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>index</title>
</head>
<body>
<h1>系统主页</h1>
<a href="${pageContext.request.contextPath}/user/logout">退出用户</a>
<ul>
<li><a href="">用户管理</a></li>
<li><a href="">订单管理</a></li>
</ul>
</body>
</html>
对应的也要在controller添加方法
@RequestMapping("logout")
public String logout(){
Subject subject = SecurityUtils.getSubject();
subject.logout();
// 退出后仍然会到登录页面
return "redirect:/login.jsp";
}
package com.mye.hl21shrio.shiro.realm;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
/** * 自定义realm */
public class CustomerRealm extends AuthorizingRealm {
// 授权
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
return null;
}
// 认证
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
// 在token中获取用户名
String principal = (String) token.getPrincipal();
System.out.println(principal);
// 模拟根据身份信息从数据库查询
if("christy".equals(principal)){
// 参数说明:用户名 | 密码 | 当前realm的名字
return new SimpleAuthenticationInfo(principal,"123456", this.getName());
}
return null;
}
}
//配置系统受限资源
//配置系统公共资源
Map<String,String> map = new HashMap<String,String>();
map.put("/user/login","anon"); // anon 设置为公共资源,放行要注意anon和authc的顺序
map.put("/index.jsp","authc"); //authc 请求这个资源需要认证和授权
//默认认证界面路径
shiroFilterFactoryBean.setLoginUrl("/login.jsp");
shiroFilterFactoryBean.setFilterChainDefinitionMap(map);
<%--解决页面乱码--%>
<%@page contentType="text/html; UTF-8" pageEncoding="UTF-8" isELIgnored="false" %>
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>register</title>
</head>
<body>
<h1>用户注册</h1>
<form action="${pageContext.request.contextPath}/user/register" method="post">
用户名:<input type="text" name="username" > <br/>
密码 : <input type="text" name="password"> <br>
<input type="submit" value="立即注册">
</form>
</body>
</html>
DROP TABLE IF EXISTS `t_user`;
create table `t_user` (
`id` int (11),
`username` varchar (32),
`password` varchar (32),
`salt` varchar (32),
`age` int (11),
`email` varchar (32),
`address` varchar (128)
);
<!-- mybatis plus -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.4.1</version>
</dependency>
<!-- Druid数据源 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.10</version>
</dependency>
<!-- Mysql -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.47</version>
</dependency>
spring:
# 设置视图模板为jsp
mvc:
view:
prefix: /
suffix: .jsp
datasource:
type: com.alibaba.druid.pool.DruidDataSource
druid:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://localhost:3306/mybatis_test?useUnicode=true&characterEncoding=UTF-8&allowMultiQueries=true&serverTimezone=UTC
username: root
password: root
# 监控统计拦截的filters
filters: stat,wall,log4j,config
# 配置初始化大小/最小/最大
initial-size: 5
min-idle: 5
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-plus:
type-aliases-package: com.mye.hl21shrio.entity
configuration:
map-underscore-to-camel-case: true
@Data
@NoArgsConstructor
@AllArgsConstructor
@TableName("t_user")
public class User implements Serializable {
/** 数据库中设置该字段自增时该注解不能少 **/
@TableId(type = IdType.AUTO)
private Integer id;
@TableField(fill = FieldFill.INSERT_UPDATE)
private String username;
@TableField(fill = FieldFill.INSERT_UPDATE)
private String password;
@TableField(fill = FieldFill.INSERT)
private String salt;
@TableField(fill = FieldFill.INSERT_UPDATE)
private Integer age;
@TableField(fill = FieldFill.INSERT_UPDATE)
private String email;
@TableField(fill = FieldFill.INSERT_UPDATE)
private String address;
}
@Mapper
public interface UserMapper extends BaseMapper<User> {
}
UserService.java
import com.mye.hl21shrio.entity.User;
public interface UserService {
/** * 用户注册 * @param user */
void register(User user);
}
UserServiceImpl.java
package com.mye.hl21shrio.service;
import com.mye.hl21shrio.entity.User;
import com.mye.hl21shrio.global.ShiroConstant;
import com.mye.hl21shrio.mapper.UserMapper;
import com.mye.hl21shrio.utils.SaltUtil;
import org.apache.shiro.crypto.hash.Md5Hash;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class UserServiceImpl implements UserService {
@Autowired
private UserMapper userMapper;
@Override
public void register(User user) {
// 生成随机盐
String salt = CryptoUtil.generateSalt();
// 保存随机盐
user.setSalt(salt);
// 生成密码
String encryptPassword = CryptoUtil.createEncryptPassword(user.getPassword(), salt);
user.setPassword(encryptPassword);
userMapper.insert(user);
}
}
CryptoUtil.java
public class CryptoUtil {
private static final RandomNumberGenerator RANDOM_NUMBER_GENERATOR = new SecureRandomNumberGenerator();
private static final String ALGORITHM_NAME = "md5";
private static final int HASH_ITERATIONS = 1024;
private static final String HMAC_SHA1 = "HmacSHA1";
public static String generateSalt() {
return RANDOM_NUMBER_GENERATOR.nextBytes().toHex();
}
public static String createEncryptPassword(String password, String salt) {
return new SimpleHash(ALGORITHM_NAME, password, ByteSource.Util.bytes(salt),
HASH_ITERATIONS).toHex();
}
public static void encryptPassword(User user) {
String newPassword = new SimpleHash(
ALGORITHM_NAME,
user.getPassword(),
ByteSource.Util.bytes(user.getSalt()),
HASH_ITERATIONS).toHex();
user.setPassword(newPassword);
}
public static String base64Encode(String s) {
if (StringUtils.isBlank(s)) {
return null;
}
return (new sun.misc.BASE64Encoder()).encode(s.getBytes());
}
public static String hmacSHA1Encode(String data, String key) throws NoSuchAlgorithmException, InvalidKeyException {
byte[] keyBytes = key.getBytes();
SecretKeySpec signingKey = new SecretKeySpec(keyBytes, HMAC_SHA1);
Mac mac = Mac.getInstance(HMAC_SHA1);
mac.init(signingKey);
byte[] rawHmac = mac.doFinal(data.getBytes());
StringBuilder sb = new StringBuilder();
for (byte b : rawHmac) {
sb.append(byteToHexString(b));
}
return sb.toString();
}
public static String urlEncode(String url) throws UnsupportedEncodingException {
if (StringUtils.isBlank(url)) {
return null;
}
return URLEncoder.encode(url, "utf-8");
}
private static String byteToHexString(byte ib) {
char[] digit = {
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'
};
char[] ob = new char[2];
ob[0] = digit[(ib >>> 4) & 0X0f];
ob[1] = digit[ib & 0X0F];
return new String(ob);
}
}
package com.mye.hl21shrio.controller;
import com.mye.hl21shrio.entity.User;
import com.mye.hl21shrio.service.UserService;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.IncorrectCredentialsException;
import org.apache.shiro.authc.UnknownAccountException;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.subject.Subject;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
@Controller
@RequestMapping("/user")
public class UserController {
@Autowired
private UserService userService;
//省略其他方法
/** * 用户注册 * @param user * @return */
@RequestMapping("register")
public String register(User user){
try {
userService.register(user);
return "redirect:/login.jsp";
} catch (Exception e) {
e.printStackTrace();
}
return "redirect:/register.jsp";
}
}
// anon 设置为公共资源,放行要注意anon和authc的顺序
map.put("/user/register","anon");
map.put("/register.jsp","anon");
访问:http://127.0.0.1:8080/register.jsp
查看数据库:
上面我们完成了基于MD5+Salt的注册流程,保存到数据库的密码都是经过加密处理的,这时候再用最初的简单密码匹配器进行equals
方法进行登录显然是不行的了,我们下面来改造一下认证的流程
UserService.java
public interface UserService {
//省略其他方法
/** * 根据用户名获取用户 */
User findUserByUserName(String userName);
}
UserServiceImpl.java
@Service
public class UserServiceImpl implements UserService {
@Autowired
private UserMapper userMapper;
//省略其他方法
@Override
public User findUserByUserName(String userName) {
return userMapper.findUserByUsername(userName);
}
}
@Mapper
public interface UserMapper extends BaseMapper<User> {
@Select("select * from t_user where username = #{userName}")
User findUserByUsername(@Param("userName") String userName);
}
CustomerRealm.java
package com.mye.hl21shrio.shiro.realm;
import com.mye.hl21shrio.entity.User;
import com.mye.hl21shrio.service.UserService;
import com.mye.hl21shrio.utils.ApplicationContextUtil;
import com.mye.hl21shrio.utils.CustomerByteSource;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.springframework.util.ObjectUtils;
/** * 自定义realm */
public class CustomerRealm extends AuthorizingRealm {
// 授权
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
return null;
}
// 认证
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
// 在token中获取用户名
String principal = (String) token.getPrincipal();
// 由于CustomerRealm并没有交由工厂管理,故不能诸如UserService
UserService userService = (UserService) ApplicationContextUtil.getBean("userServiceImpl");
User user = userService.findUserByUserName(principal);
if(!ObjectUtils.isEmpty(user)){
return new SimpleAuthenticationInfo(
user.getUsername(),
user.getPassword(),
ByteSource.Util.bytes(user.getSalt()),
this.getName());
}
return null;
}
}
ApplicationContextUtil.java
package com.mye.hl21shrio.utils;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;
@Component
public class ApplicationContextUtil implements ApplicationContextAware {
public static ApplicationContext context;
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
context = applicationContext;
}
/** * 根据工厂中的类名获取类实例 */
public static Object getBean(String beanName){
return context.getBean(beanName);
}
}
ShiroConfiguration.java
@Configuration
public class ShiroConfiguration {
//省略其他
@Bean
public Realm getRealm(){
CustomerRealm customerRealm = new CustomerRealm();
//设置密码匹配器
HashedCredentialsMatcher credentialsMatcher = new HashedCredentialsMatcher();
//设置加密方式
credentialsMatcher.setHashAlgorithmName("MD5");
//设置散列次数
credentialsMatcher.setHashIterations(1024);
customerRealm.setCredentialsMatcher(credentialsMatcher);
return customerRealm;
}
}
这里要注意设置的散列数要和生成密码时候的散列数一致
数据库
# 用户表上面已经有了:t_user
/*Table structure for table `t_role` */
DROP TABLE IF EXISTS `t_role`;
CREATE TABLE `t_role` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(64) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8;
/*Table structure for table `t_user_role` */
DROP TABLE IF EXISTS `t_user_role`;
CREATE TABLE `t_user_role` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`user_id` int(8) DEFAULT NULL,
`role_id` int(8) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=7 DEFAULT CHARSET=utf8;
这是t_user
表,这里有俩个通过认证的用户
t_role
角色表中有两种角色admin
和user
我们为用户zhangsan
赋予了admin
的权限,lisi
赋予了user
的权限
index.jsp
<%--解决页面乱码--%>
<%@page contentType="text/html; UTF-8" pageEncoding="UTF-8" isELIgnored="false" %>
<%@taglib prefix="shiro" uri="http://shiro.apache.org/tags" %>
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>index</title>
</head>
<body>
<h1>系统主页</h1>
<a href="${pageContext.request.contextPath}/user/logout">退出用户</a>
<ul>
<%-- admin角色的用户能同时拥有用户管理和订单管理的权限,user角色的用户只拥有订单管理的权限 --%>
<shiro:hasRole name="admin">
<li>
<a href="">用户管理</a>
</li>
</shiro:hasRole>
<shiro:hasAnyRoles name="admin,user">
<li><a href="">订单管理</a></li>
</shiro:hasAnyRoles>
</ul>
</body>
</html>
实体类User.java与Role.java
@Data
@NoArgsConstructor
@AllArgsConstructor
@TableName("t_user")
public class User implements Serializable {
/** 其他属性省略 **/
@TableField(exist = false)
private List<Role> roles = new ArrayList<>();
}
@Data
@NoArgsConstructor
@AllArgsConstructor
@TableName("t_role")
public class Role implements Serializable {
/** 数据库中设置该字段自增时该注解不能少 **/
@TableId(type = IdType.AUTO)
private Integer id;
@TableField(fill = FieldFill.INSERT_UPDATE)
private String name;
}
mapper
@Mapper
public interface RoleMapper extends BaseMapper<Role> {
/** * 根据用户id查询用户的角色 */
@Select("select r.id,r.name from t_role r left join t_user_role ur on ur.role_id = r.id where ur.user_id = #{userId}")
List<Role> getRolesByUserId(Integer userId);
}
RoleService.java和RoleServiceImpl
public interface RoleService {
/** * 根据用户id获取用户的角色集合 */
List<Role> getRolesByUserId(Integer userId);
}
@Service
public class RoleServiceImpl implements RoleService {
@Autowired
private RoleMapper roleMapper;
@Override
public List<Role> getRolesByUserId(Integer userId) {
return roleMapper.getRolesByUserId(userId);
}
}
Realm中实现授权
public class CustomerRealm extends AuthorizingRealm {
// 授权
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
// 获取主身份信息
String principal = (String) principalCollection.getPrimaryPrincipal();
// 根据主身份信息获取角色信息
UserService userService = (UserService) ApplicationContextUtil.getBean("userServiceImpl");
User user = userService.findUserByUserName(principal);
RoleService roleService = (RoleService) ApplicationContextUtil.getBean("roleServiceImpl");
List<Role> roles = roleService.getRolesByUserId(user.getId());
if(!CollectionUtils.isEmpty(roles)){
SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
roles.forEach(role -> {
simpleAuthorizationInfo.addRole(role.getName());
});
return simpleAuthorizationInfo;
}
return null;
}
//省略认证
}
测试
上图可以看到admin角色的用户登录系统后能够看到用户管理和订单管理,user角色的用户只能看到订单管理
数据库
/*Table structure for table `t_permission` */
DROP TABLE IF EXISTS `t_permission`;
CREATE TABLE `t_permission` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(128) DEFAULT NULL,
`url` varchar(255) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8;
/*Table structure for table `t_role_permission` */
DROP TABLE IF EXISTS `t_role_permission`;
CREATE TABLE `t_role_permission` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`role_id` int(11) DEFAULT NULL,
`permission_id` int(11) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=6 DEFAULT CHARSET=utf8;
视图层-index.jsp
<%--解决页面乱码--%>
<%@page contentType="text/html; UTF-8" pageEncoding="UTF-8" isELIgnored="false" %>
<%@taglib prefix="shiro" uri="http://shiro.apache.org/tags" %>
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>index</title>
</head>
<body>
<h1>系统主页</h1>
<a href="${pageContext.request.contextPath}/user/logout">退出用户</a>
<ul>
<%-- admin角色的用户能同时拥有用户管理和订单管理的权限,user角色的用户只拥有订单管理的权限 --%>
<shiro:hasRole name="admin">
<li>
<a href="">用户管理</a>
</li>
</shiro:hasRole>
<%-- admin角色的用户对订单有增删改查的权限,user角色的用户只能查看订单 --%>
<shiro:hasAnyRoles name="admin,user">
<li>
<a href="">订单管理</a>
<ul>
<shiro:hasPermission name="order:add:*">
<li><a href="">新增</a></li>
</shiro:hasPermission>
<shiro:hasPermission name="order:del:*">
<li><a href="">删除</a></li>
</shiro:hasPermission>
<shiro:hasPermission name="order:upd:*">
<li><a href="">修改</a></li>
</shiro:hasPermission>
<shiro:hasPermission name="order:find:*">
<li><a href="">查询</a></li>
</shiro:hasPermission>
</ul>
</li>
</shiro:hasAnyRoles>
</ul>
</body>
</html>
实体类Role.java与Permission.java
@Data
@NoArgsConstructor
@AllArgsConstructor
@TableName("t_role")
public class Role implements Serializable {
//其他省略
@TableField(exist = false)
private List<Permission> permissions = new ArrayList<>();
}
@Data
@NoArgsConstructor
@AllArgsConstructor
@TableName("t_permission")
public class Permission implements Serializable {
/** 数据库中设置该字段自增时该注解不能少 **/
@TableId(type = IdType.AUTO)
private Integer id;
@TableField(fill = FieldFill.INSERT_UPDATE)
private String name;
@TableField(fill = FieldFill.INSERT_UPDATE)
private String url;
}
mapper
@Mapper
public interface PermissionMapper extends BaseMapper<Permission> {
/** * 根据角色id查询权限 */
@Select("select p.id,p.name,p.url from t_permission p left join t_role_permission rp on rp.permission_id = p.id where rp.role_id = #{roleId}")
List<Permission> getPermissionsByRoleId(Integer roleId);
}
service层
public interface PermissionService {
/** * 根据角色id获取权限集合 */
List<Permission> getPermissionsByRoleId(Integer roleId);
}
@Service
public class PermissionServiceImpl implements PermissionService {
@Autowired
private PermissionMapper permissionMapper;
@Override
public List<Permission> getPermissionsByRoleId(Integer roleId) {
return permissionMapper.getPermissionsByRoleId(roleId);
}
}
Realm中实现授权
public class CustomerRealm extends AuthorizingRealm {
// 授权
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
// 获取主身份信息
String principal = (String) principalCollection.getPrimaryPrincipal();
// 根据主身份信息获取角色信息
UserService userService = (UserService) ApplicationContextUtil.getBean("userServiceImpl");
User user = userService.findUserByUserName(principal);
RoleService roleService = (RoleService) ApplicationContextUtil.getBean("roleServiceImpl");
List<Role> roles = roleService.getRolesByUserId(user.getId());
if(!CollectionUtils.isEmpty(roles)){
SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
roles.forEach(role -> {
simpleAuthorizationInfo.addRole(role.getName());
PermissionService permissionService = (PermissionService) ApplicationContextUtil.getBean("permissionServiceImpl");
List<Permission> permissions = permissionService.getPermissionsByRoleId(role.getId());
if(!CollectionUtils.isEmpty(permissions)){
permissions.forEach(permission -> {
simpleAuthorizationInfo.addStringPermission(permission.getName());
});
}
});
return simpleAuthorizationInfo;
}
return null;
}
// 省略认证
}
测试
shiro提供了缓存管理器,这样在用户第一次认证授权后访问其受限资源的时候就不用每次查询数据库从而达到减轻数据压力的作用,使用shiro的缓存管理器也很简单
<!--引入shiro和ehcache-->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-ehcache</artifactId>
<version>1.4.0</version>
</dependency>
package com.mye.hl21shrio.config;
import com.mye.hl21shrio.global.ShiroConstant;
import com.mye.hl21shrio.shiro.realm.CustomerRealm;
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.apache.shiro.cache.ehcache.EhCacheManager;
import org.apache.shiro.realm.Realm;
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.HashMap;
import java.util.Map;
/** * Shiro的核心配置类,用来整合shiro框架 */
@Configuration
public class ShiroConfiguration {
//1.创建shiroFilter //负责拦截所有请求
@Bean(name = "shiroFilterFactoryBean")
public ShiroFilterFactoryBean shiroFilterFactoryBean(DefaultWebSecurityManager defaultWebSecurityManager){
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
//给filter设置安全管理器
shiroFilterFactoryBean.setSecurityManager(defaultWebSecurityManager);
//配置系统受限资源
//配置系统公共资源
Map<String,String> map = new HashMap<String,String>();
map.put("/user/register","anon");
map.put("/user/login","anon");//anon 设置为公共资源,放行要注意anon和authc的顺序
map.put("/index.jsp","authc");//authc 请求这个资源需要认证和授权
//身份认证失败,则跳转到登录页面的配置 没有登录的用户请求需要登录的页面时自动跳转到登录页面,
// 不是必须的属性,不输入地址的话会自动寻找项目web项目的根目录下的”/login.jsp”页面。
shiroFilterFactoryBean.setLoginUrl("/login.jsp");
shiroFilterFactoryBean.setFilterChainDefinitionMap(map);
return shiroFilterFactoryBean;
}
//2.创建安全管理器
@Bean(name = "defaultWebSecurityManager")
public DefaultWebSecurityManager defaultWebSecurityManager(Realm realm){
DefaultWebSecurityManager defaultWebSecurityManager = new DefaultWebSecurityManager();
//给安全管理器设置
defaultWebSecurityManager.setRealm(realm);
return defaultWebSecurityManager;
}
//3.创建自定义realm
@Bean
public Realm getRealm(@Qualifier("ehCacheManager") EhCacheManager ehCacheManager){
CustomerRealm customerRealm = new CustomerRealm();
//设置密码匹配器
HashedCredentialsMatcher credentialsMatcher = new HashedCredentialsMatcher();
//设置加密方式
credentialsMatcher.setHashAlgorithmName("MD5");
//设置散列次数
credentialsMatcher.setHashIterations(1024);
customerRealm.setCredentialsMatcher(credentialsMatcher);
//设置缓存管理器
customerRealm.setCacheManager(ehCacheManager);
//开启全局缓存
customerRealm.setCachingEnabled(true);
//开启认证缓存并指定缓存名称
customerRealm.setAuthenticationCachingEnabled(true);
customerRealm.setAuthenticationCacheName("authenticationCache");
// 开启授权缓存并指定缓存名称
customerRealm.setAuthorizationCachingEnabled(true);
customerRealm.setAuthorizationCacheName("authorizationCache");
return customerRealm;
}
@Bean(name = "ehCacheManager")
public EhCacheManager ehCacheManager() {
EhCacheManager cacheManager = new EhCacheManager();
cacheManager.setCacheManagerConfigFile("classpath:ehcache.xml");
return cacheManager;
}
}
<ehcache name="spring_boot_shiro_ehcache" updateCheck="false">
<diskStore path="java.io.tmpdir"/>
<defaultCache maxElementsInMemory="100000" eternal="false" overflowToDisk="true" timeToIdleSeconds="0" timeToLiveSeconds="0" diskPersistent="false" diskExpiryThreadIntervalSeconds="120"/>
<!-- See http://ehcache.sourceforge.net/documentation/#mozTocId258426 for how to configure caching for your objects -->
</ehcache>
这样就将EhCache集成进来了,但是shiro的这个缓存是本地缓存,也就是说当程序宕机重启后仍然需要从数据库加载数据,不能实现分布式缓存的功能。下面我们来集成redis来实现缓存
<!-- 引入redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>2.9.0</version>
</dependency>
public class RedisCacheManager extends AbstractCacheManager {
private final RedisTemplate<Serializable, Session> redisTemplate;
public RedisCacheManager(RedisTemplate<Serializable, Session> redisTemplate) {
this.redisTemplate = redisTemplate;
}
@Override
protected Cache<Serializable, Session> createCache(String cacheName) throws CacheException {
return new RedisCache<>(redisTemplate,cacheName);
}
}
import org.apache.shiro.cache.Cache;
import org.apache.shiro.cache.CacheException;
import org.springframework.data.redis.core.RedisTemplate;
import java.io.Serializable;
import java.util.Collection;
import java.util.Set;
public class RedisCache<K,V> implements Cache<K,V> {
private String cacheName;
private final RedisTemplate<Serializable, V> redisTemplate;
public RedisCache(RedisTemplate<Serializable, V> redisTemplate) {
this.redisTemplate = redisTemplate;
}
public RedisCache(RedisTemplate<Serializable, V> redisTemplate, String name) {
this(redisTemplate);
this.cacheName = name;
}
@Override
public V get(K k) throws CacheException {
return (V) redisTemplate.opsForHash().get(this.cacheName,k.toString());
}
@Override
public V put(K k, V v) throws CacheException {
redisTemplate.opsForHash().put(this.cacheName,k.toString(),v);
return null;
}
@Override
public V remove(K k) throws CacheException {
return (V) redisTemplate.opsForHash().delete(this.cacheName,k.toString());
}
@Override
public void clear() throws CacheException {
redisTemplate.delete(this.cacheName);
}
@Override
public int size() {
return redisTemplate.opsForHash().size(this.cacheName).intValue();
}
@Override
public Set<K> keys() {
return (Set<K>) redisTemplate.opsForHash().keys(this.cacheName);
}
@Override
public Collection<V> values() {
return (Collection<V>) redisTemplate.opsForHash().values(this.cacheName);
}
}
package com.mye.hl21shrio.shiro.cache;
import org.apache.shiro.codec.Base64;
import org.apache.shiro.codec.CodecSupport;
import org.apache.shiro.codec.Hex;
import org.apache.shiro.util.ByteSource;
import java.io.Serializable;
import java.util.Arrays;
public class ShiroByteSource implements ByteSource, Serializable {
private static final long serialVersionUID = -6814382603612799610L;
private volatile byte[] bytes;
private String cachedHex;
private String cachedBase64;
public ShiroByteSource() {
}
public ShiroByteSource(String string) {
this.bytes = CodecSupport.toBytes(string);
}
public void setBytes(byte[] bytes) {
this.bytes = bytes;
}
@Override
public byte[] getBytes() {
return this.bytes;
}
@Override
public String toHex() {
if ( this.cachedHex == null ) {
this.cachedHex = Hex.encodeToString(getBytes());
}
return this.cachedHex;
}
@Override
public String toBase64() {
if ( this.cachedBase64 == null ) {
this.cachedBase64 = Base64.encodeToString(getBytes());
}
return this.cachedBase64;
}
@Override
public boolean isEmpty() {
return this.bytes == null || this.bytes.length == 0;
}
@Override
public String toString() {
return toBase64();
}
@Override
public int hashCode() {
if (this.bytes == null || this.bytes.length == 0) {
return 0;
}
return Arrays.hashCode(this.bytes);
}
@Override
public boolean equals(Object o) {
if (o == this) {
return true;
}
if (o instanceof ByteSource) {
ByteSource bs = (ByteSource) o;
return Arrays.equals(getBytes(), bs.getBytes());
}
return false;
}
public static ByteSource of(String string) {
return new ShiroByteSource(string);
}
}
然后修改CustomerRealm的认证方法,不然会提示序列化失败,因为源码中的ByteSource没有序列化
package com.mye.hl21shrio.config;
import com.mye.hl21shrio.shiro.cache.RedisCacheManager;
import com.mye.hl21shrio.shiro.realm.CustomerRealm;
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.apache.shiro.cache.ehcache.EhCacheManager;
import org.apache.shiro.realm.Realm;
import org.apache.shiro.session.Session;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.DependsOn;
import org.springframework.data.redis.connection.RedisStandaloneConfiguration;
import org.springframework.data.redis.connection.jedis.JedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import java.io.Serializable;
import java.util.HashMap;
import java.util.Map;
/** * Shiro的核心配置类,用来整合shiro框架 */
@Configuration
public class ShiroConfiguration {
//1.创建shiroFilter //负责拦截所有请求
@Bean(name = "shiroFilterFactoryBean")
public ShiroFilterFactoryBean shiroFilterFactoryBean(DefaultWebSecurityManager defaultWebSecurityManager){
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
//给filter设置安全管理器
shiroFilterFactoryBean.setSecurityManager(defaultWebSecurityManager);
//配置系统受限资源
//配置系统公共资源
Map<String,String> map = new HashMap<String,String>();
map.put("/user/register","anon");
map.put("/user/login","anon");//anon 设置为公共资源,放行要注意anon和authc的顺序
map.put("/index.jsp","authc");//authc 请求这个资源需要认证和授权
//身份认证失败,则跳转到登录页面的配置 没有登录的用户请求需要登录的页面时自动跳转到登录页面,
// 不是必须的属性,不输入地址的话会自动寻找项目web项目的根目录下的”/login.jsp”页面。
shiroFilterFactoryBean.setLoginUrl("/login.jsp");
shiroFilterFactoryBean.setFilterChainDefinitionMap(map);
return shiroFilterFactoryBean;
}
//2.创建安全管理器
@Bean(name = "defaultWebSecurityManager")
public DefaultWebSecurityManager defaultWebSecurityManager(Realm realm){
DefaultWebSecurityManager defaultWebSecurityManager = new DefaultWebSecurityManager();
//给安全管理器设置
defaultWebSecurityManager.setRealm(realm);
return defaultWebSecurityManager;
}
//3.创建自定义realm
@Bean
public Realm getRealm(){
CustomerRealm customerRealm = new CustomerRealm();
//设置密码匹配器
HashedCredentialsMatcher credentialsMatcher = new HashedCredentialsMatcher();
//设置加密方式
credentialsMatcher.setHashAlgorithmName("MD5");
//设置散列次数
credentialsMatcher.setHashIterations(1024);
customerRealm.setCredentialsMatcher(credentialsMatcher);
//设置缓存管理器
if (connectionFactory()!= null){
customerRealm.setCacheManager(redisCacheManager());
}else {
customerRealm.setCacheManager(ehCacheManager());
}
//开启全局缓存
customerRealm.setCachingEnabled(true);
//开启认证缓存并指定缓存名称
customerRealm.setAuthenticationCachingEnabled(true);
customerRealm.setAuthenticationCacheName("authenticationCache");
// 开启授权缓存并指定缓存名称
customerRealm.setAuthorizationCachingEnabled(true);
customerRealm.setAuthorizationCacheName("authorizationCache");
return customerRealm;
}
@Bean(name = "ehCacheManager")
public EhCacheManager ehCacheManager() {
EhCacheManager cacheManager = new EhCacheManager();
cacheManager.setCacheManagerConfigFile("classpath:ehcache.xml");
return cacheManager;
}
@Bean("connectionFactory")
public JedisConnectionFactory connectionFactory() {
String redisHost = "127.0.0.1";
int redisPort = 6379;
//redis配置单节点
RedisStandaloneConfiguration redisStandaloneConfiguration = new RedisStandaloneConfiguration(redisHost,redisPort);
return new JedisConnectionFactory(redisStandaloneConfiguration);
}
@Bean(name = "redisTemplate")
public RedisTemplate<Serializable, Session> redisTemplate(){
RedisTemplate<Serializable, Session> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(connectionFactory());
return redisTemplate;
}
@Bean(name = "redisCacheManager")
@DependsOn(value = "redisTemplate")
public RedisCacheManager redisCacheManager(){
return new RedisCacheManager(redisTemplate());
}
}
上图可以看到我们用户登录以后用户的认证和授权数据已经缓存到redis了,这个时候即使程序重启,redis中的缓存数据也不会删除,除非用户自己退出登录。
<%--解决页面乱码--%>
<%@page contentType="text/html; UTF-8" pageEncoding="UTF-8" isELIgnored="false" %>
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>login</title>
</head>
<body>
<h1>用户登录</h1>
<form action="${pageContext.request.contextPath}/user/login" method="post">
用户名:<input type="text" name="username" > <br/>
密码 : <input type="text" name="password"> <br>
验证码: <input type="text" name="verifyCode"><img src="${pageContext.request.contextPath}/user/getImage" alt=""><br>
<input type="submit" value="登录">
</form>
</body>
</html>
package com.mye.hl21shrio.utils;
import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.geom.AffineTransform;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.Arrays;
import java.util.Random;
public class VerifyCodeUtil {
//使用到Algerian字体,系统里没有的话需要安装字体,字体只显示大写,去掉了1,0,i,o几个容易混淆的字符
public static final String VERIFY_CODES = "23456789ABCDEFGHJKLMNPQRSTUVWXYZ";
private static Random random = new Random();
/** * 使用系统默认字符源生成验证码 * @param verifySize 验证码长度 * @return */
public static String generateVerifyCode(int verifySize) {
return generateVerifyCode(verifySize, VERIFY_CODES);
}
/** * 使用指定源生成验证码 * @param verifySize 验证码长度 * @param sources 验证码字符源 * @return */
public static String generateVerifyCode(int verifySize, String sources) {
if (sources == null || sources.length() == 0) {
sources = VERIFY_CODES;
}
int codesLen = sources.length();
Random rand = new Random(System.currentTimeMillis());
StringBuilder verifyCode = new StringBuilder(verifySize);
for (int i = 0; i < verifySize; i++) {
verifyCode.append(sources.charAt(rand.nextInt(codesLen - 1)));
}
return verifyCode.toString();
}
/** * 生成随机验证码文件,并返回验证码值 * @param w * @param h * @param outputFile * @param verifySize * @return * @throws IOException */
public static String outputVerifyImage(int w, int h, File outputFile, int verifySize) throws IOException {
String verifyCode = generateVerifyCode(verifySize);
outputImage(w, h, outputFile, verifyCode);
return verifyCode;
}
/** * 输出随机验证码图片流,并返回验证码 * @param w * @param h * @param os * @param verifySize * @return * @throws IOException */
public static String outputVerifyImage(int w, int h, OutputStream os, int verifySize) throws IOException {
String verifyCode = generateVerifyCode(verifySize);
outputImage(w, h, os, verifyCode);
return verifyCode;
}
/** * 生成指定验证码图像文件 * @param w * @param h * @param outputFile * @param code * @throws IOException */
public static void outputImage(int w, int h, File outputFile, String code) throws IOException {
if (outputFile == null) {
return;
}
File dir = outputFile.getParentFile();
if (!dir.exists()) {
dir.mkdirs();
}
try {
outputFile.createNewFile();
FileOutputStream fos = new FileOutputStream(outputFile);
outputImage(w, h, fos, code);
fos.close();
} catch (IOException e) {
throw e;
}
}
/** * 输出指定验证码图片流 * @param w * @param h * @param os * @param code * @throws IOException */
public static void outputImage(int w, int h, OutputStream os, String code) throws IOException {
int verifySize = code.length();
BufferedImage image = new BufferedImage(w, h, BufferedImage.TYPE_INT_RGB);
Random rand = new Random();
Graphics2D g2 = image.createGraphics();
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
Color[] colors = new Color[5];
Color[] colorSpaces = new Color[]{Color.WHITE, Color.CYAN, Color.GRAY, Color.LIGHT_GRAY, Color.MAGENTA, Color.ORANGE, Color.PINK, Color.YELLOW};
float[] fractions = new float[colors.length];
for (int i = 0; i < colors.length; i++) {
colors[i] = colorSpaces[rand.nextInt(colorSpaces.length)];
fractions[i] = rand.nextFloat();
}
Arrays.sort(fractions);
g2.setColor(Color.GRAY);// 设置边框色
g2.fillRect(0, 0, w, h);
Color c = getRandColor(200, 250);
g2.setColor(c);// 设置背景色
g2.fillRect(0, 2, w, h - 4);
//绘制干扰线
Random random = new Random();
g2.setColor(getRandColor(160, 200));// 设置线条的颜色
for (int i = 0; i < 20; i++) {
int x = random.nextInt(w - 1);
int y = random.nextInt(h - 1);
int xl = random.nextInt(6) + 1;
int yl = random.nextInt(12) + 1;
g2.drawLine(x, y, x + xl + 40, y + yl + 20);
}
// 添加噪点
float yawpRate = 0.05f;// 噪声率
int area = (int) (yawpRate * w * h);
for (int i = 0; i < area; i++) {
int x = random.nextInt(w);
int y = random.nextInt(h);
int rgb = getRandomIntColor();
image.setRGB(x, y, rgb);
}
shear(g2, w, h, c);// 使图片扭曲
g2.setColor(getRandColor(100, 160));
int fontSize = h - 4;
Font font = new Font("Algerian", Font.ITALIC, fontSize);
g2.setFont(font);
char[] chars = code.toCharArray();
for (int i = 0; i < verifySize; i++) {
AffineTransform affine = new AffineTransform();
affine.setToRotation(Math.PI / 4 * rand.nextDouble() * (rand.nextBoolean() ? 1 : -1), (w / verifySize) * i + fontSize / 2, h / 2);
g2.setTransform(affine);
g2.drawChars(chars, i, 1, ((w - 10) / verifySize) * i + 5, h / 2 + fontSize / 2 - 10);
}
g2.dispose();
ImageIO.write(image, "jpg", os);
}
private static Color getRandColor(int fc, int bc) {
if (fc > 255) {
fc = 255;
}
if (bc > 255) {
bc = 255;
}
int r = fc + random.nextInt(bc - fc);
int g = fc + random.nextInt(bc - fc);
int b = fc + random.nextInt(bc - fc);
return new Color(r, g, b);
}
private static int getRandomIntColor() {
int[] rgb = getRandomRgb();
int color = 0;
for (int c : rgb) {
color = color << 8;
color = color | c;
}
return color;
}
private static int[] getRandomRgb() {
int[] rgb = new int[3];
for (int i = 0; i < 3; i++) {
rgb[i] = random.nextInt(255);
}
return rgb;
}
private static void shear(Graphics g, int w1, int h1, Color color) {
shearX(g, w1, h1, color);
shearY(g, w1, h1, color);
}
private static void shearX(Graphics g, int w1, int h1, Color color) {
int period = random.nextInt(2);
boolean borderGap = true;
int frames = 1;
int phase = random.nextInt(2);
for (int i = 0; i < h1; i++) {
double d = (double) (period >> 1) * Math.sin((double) i / (double) period + (6.2831853071795862D * (double) phase) / (double) frames);
g.copyArea(0, i, w1, 1, (int) d, 0);
if (borderGap) {
g.setColor(color);
g.drawLine((int) d, i, 0, i);
g.drawLine((int) d + w1, i, w1, i);
}
}
}
private static void shearY(Graphics g, int w1, int h1, Color color) {
int period = random.nextInt(40) + 10; // 50;
boolean borderGap = true;
int frames = 20;
int phase = 7;
for (int i = 0; i < w1; i++) {
double d = (double) (period >> 1) * Math.sin((double) i / (double) period + (6.2831853071795862D * (double) phase) / (double) frames);
g.copyArea(i, 0, 1, h1, 0, (int) d);
if (borderGap) {
g.setColor(color);
g.drawLine(i, (int) d, i, 0);
g.drawLine(i, (int) d + h1, i, h1);
}
}
}
public static void main(String[] args) throws IOException {
//获取验证码
String s = generateVerifyCode(4);
//将验证码放入图片中
outputImage(260, 60, new File(""), s);
System.out.println(s);
}
}
UserController.java中有两处变动,其一是需要一个生成验证码的方法并输出到页面;其二是要修改认证的流程,先进行验证码的校验,验证码校验通过以后才可以进行Shiro的认证
package com.mye.hl21shrio.controller;
import com.mye.hl21shrio.entity.User;
import com.mye.hl21shrio.service.UserService;
import com.mye.hl21shrio.utils.VerifyCodeUtil;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.IncorrectCredentialsException;
import org.apache.shiro.authc.UnknownAccountException;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.subject.Subject;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.io.IOException;
@Controller
@RequestMapping("/user")
public class UserController {
@Autowired
private UserService userService;
@RequestMapping("/login")
public String login(String username,String password, String verifyCode,HttpSession session){
// 校验验证码
String verifyCodes = (String) session.getAttribute("verifyCode");
// 获取当前登录用户
Subject subject = SecurityUtils.getSubject();
try {
if(verifyCodes.equalsIgnoreCase(verifyCode)){
subject.login(new UsernamePasswordToken(username,password));
return "redirect:/index.jsp";
} else {
throw new RuntimeException("验证码错误");
}
} catch (UnknownAccountException e) {
e.printStackTrace();
System.out.println("用户名错误~");
} catch (IncorrectCredentialsException e) {
e.printStackTrace();
System.out.println("密码错误~");
} catch (Exception e) {
e.printStackTrace();
}
return "redirect:/login.jsp";
}
@RequestMapping("getImage")
public void getImage(HttpSession session, HttpServletResponse response) throws IOException {
//生成验证码
String verifyCode = VerifyCodeUtil.generateVerifyCode(4);
//验证码放入session
session.setAttribute("verifyCode",verifyCode);
//验证码存入图片
ServletOutputStream os = response.getOutputStream();
response.setContentType("image/png");
VerifyCodeUtil.outputImage(180,40,os,verifyCode);
}
}
map.put("/user/getImage","anon");
<!-- 配置 JWT -->
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.4.1</version>
</dependency>
本例中与一般Shiro配置不一样
package com.mye.hl21shrio.config;
import com.mye.hl21shrio.shiro.Authenticator.MultiRealmAuthenticator;
import com.mye.hl21shrio.shiro.cache.RedisCacheManager;
import com.mye.hl21shrio.shiro.filter.JwtCredentialsMatcher;
import com.mye.hl21shrio.shiro.filter.JwtFilter;
import com.mye.hl21shrio.shiro.realm.DbShiroRealm;
import com.mye.hl21shrio.shiro.realm.JwtRealm;
import org.apache.shiro.authc.credential.CredentialsMatcher;
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.apache.shiro.authc.pam.AuthenticationStrategy;
import org.apache.shiro.authc.pam.FirstSuccessfulStrategy;
import org.apache.shiro.authc.pam.ModularRealmAuthenticator;
import org.apache.shiro.cache.ehcache.EhCacheManager;
import org.apache.shiro.mgt.DefaultSessionStorageEvaluator;
import org.apache.shiro.mgt.DefaultSubjectDAO;
import org.apache.shiro.mgt.SessionStorageEvaluator;
import org.apache.shiro.realm.Realm;
import org.apache.shiro.session.Session;
import org.apache.shiro.session.SessionListener;
import org.apache.shiro.spring.LifecycleBeanPostProcessor;
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.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.DependsOn;
import org.springframework.data.redis.connection.RedisStandaloneConfiguration;
import org.springframework.data.redis.connection.jedis.JedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.web.filter.DelegatingFilterProxy;
import javax.servlet.DispatcherType;
import javax.servlet.Filter;
import java.io.Serializable;
import java.util.*;
/** * Shiro的核心配置类,用来整合shiro框架 */
@Configuration
public class ShiroConfiguration {
@Autowired(required = false)
Collection<SessionListener> sessionListeners;
@Bean
public FilterRegistrationBean<Filter> filterRegistrationBean() {
FilterRegistrationBean<Filter> filterRegistration = new FilterRegistrationBean<Filter>();
filterRegistration.setFilter(new DelegatingFilterProxy("shiroFilterFactoryBean"));
filterRegistration.setEnabled(true);
filterRegistration.addUrlPatterns("/*");
filterRegistration.setDispatcherTypes(DispatcherType.REQUEST);
return filterRegistration;
}
//1.创建shiroFilter //负责拦截所有请求
@Bean(name = "shiroFilterFactoryBean")
public ShiroFilterFactoryBean shiroFilterFactoryBean(DefaultWebSecurityManager defaultWebSecurityManager){
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
//给filter设置安全管理器
shiroFilterFactoryBean.setSecurityManager(defaultWebSecurityManager);
//添加jwt专用过滤器,拦截除/user/login和/user/user/logout外的请求
LinkedHashMap<String, Filter> filterMap = new LinkedHashMap<>();
filterMap.put("jwtFilter",new JwtFilter());
shiroFilterFactoryBean.setFilters(filterMap);
//配置系统受限资源
//配置系统公共资源
LinkedHashMap<String, String> map = new LinkedHashMap<>();
//anon 设置为公共资源,放行要注意anon和authc的顺序
map.put("/user/register","anon");
map.put("/user/getImage","anon");
map.put("/user/login","anon");//anon 设置为公共资源,
map.put("/user/logout","logout");//logout 退出登录
map.put("/logout","logout");//logout 退出登录
map.put("/login","anon");
map.put("/**","jwtFilter,authc");//authc 请求这个资源需要认证和授权
//身份认证失败,则跳转到登录页面的配置 没有登录的用户请求需要登录的页面时自动跳转到登录页面,
// 不是必须的属性,不输入地址的话会自动寻找项目web项目的根目录下的”/login.jsp”页面。
// shiroFilterFactoryBean.setLoginUrl("/login.jsp");
shiroFilterFactoryBean.setFilterChainDefinitionMap(map);
return shiroFilterFactoryBean;
}
//2.创建安全管理器
@Bean(name = "defaultWebSecurityManager")
public DefaultWebSecurityManager defaultWebSecurityManager(){
DefaultWebSecurityManager defaultWebSecurityManager = new DefaultWebSecurityManager();
//Authenticator
defaultWebSecurityManager.setAuthenticator(authenticator());
//Realm
List<Realm> realmList = new ArrayList<>(16);
realmList.add(jwtRealm());
realmList.add(dbShiroRealm());
defaultWebSecurityManager.setRealms(realmList);
//关闭shiro自带的session
// 3.关闭shiro自带的session
DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
subjectDAO.setSessionStorageEvaluator(sessionStorageEvaluator());
defaultWebSecurityManager.setSubjectDAO(subjectDAO);
if (connectionFactory() == null){
defaultWebSecurityManager.setCacheManager(ehCacheManager());
}else {
defaultWebSecurityManager.setCacheManager(redisCacheManager());
}
return defaultWebSecurityManager;
}
/** * 禁用session, 不保存用户登录状态。保证每次请求都重新认证 */
@Bean
protected SessionStorageEvaluator sessionStorageEvaluator() {
DefaultSessionStorageEvaluator sessionStorageEvaluator = new DefaultSessionStorageEvaluator();
sessionStorageEvaluator.setSessionStorageEnabled(false);
return sessionStorageEvaluator;
}
@Bean(name = "ehCacheManager")
public EhCacheManager ehCacheManager() {
EhCacheManager cacheManager = new EhCacheManager();
cacheManager.setCacheManagerConfigFile("classpath:ehcache.xml");
return cacheManager;
}
/** * 配置 ModularRealmAuthenticator */
@Bean
public ModularRealmAuthenticator authenticator() {
ModularRealmAuthenticator authenticator = new MultiRealmAuthenticator();
// 设置多 Realm的认证策略,默认 AtLeastOneSuccessfulStrategy
AuthenticationStrategy strategy = new FirstSuccessfulStrategy();
authenticator.setAuthenticationStrategy(strategy);
return authenticator;
}
/** * JwtRealm 配置,需实现 Realm 接口 */
@Bean
public JwtRealm jwtRealm() {
JwtRealm jwtRealm = new JwtRealm();
// 设置加密算法
CredentialsMatcher credentialsMatcher = new JwtCredentialsMatcher();
// 设置加密次数
jwtRealm.setCredentialsMatcher(credentialsMatcher);
return jwtRealm;
}
/** * 用于用户名密码登录时认证的realm */
@Bean(name = "dbShiroRealm")
public Realm dbShiroRealm(){
DbShiroRealm dbShiroRealm = new DbShiroRealm();
//设置密码匹配器
HashedCredentialsMatcher credentialsMatcher = new HashedCredentialsMatcher();
//设置加密方式
credentialsMatcher.setHashAlgorithmName("MD5");
//设置散列次数
credentialsMatcher.setHashIterations(1024);
dbShiroRealm.setCredentialsMatcher(credentialsMatcher);
dbShiroRealm.setCachingEnabled(true);
//开启身份验证缓存
dbShiroRealm.setAuthenticationCachingEnabled(true);
dbShiroRealm.setAuthenticationCacheName("authenticationCache");
//开启权限缓存
dbShiroRealm.setAuthorizationCachingEnabled(true);
dbShiroRealm.setAuthorizationCacheName("authorizationCache");
return dbShiroRealm;
}
/** * 交由 Spring 来自动地管理 Shiro-Bean 的生命周期 */
@Bean
public static LifecycleBeanPostProcessor getLifecycleBeanPostProcessor() {
return new LifecycleBeanPostProcessor();
}
/** * 为 Spring-Bean 开启对 Shiro 注解的支持 */
@Bean(name = "authorizationAttributeSourceAdvisor")
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(@Qualifier("defaultWebSecurityManager") DefaultWebSecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
return authorizationAttributeSourceAdvisor;
}
/** * 扫描上下文,寻找所有的Advistor(通知器) * 将这些Advisor应用到所有符合切入点的Bean中。 */
@Bean(name = "defaultAdvisorAutoProxyCreator")
public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
DefaultAdvisorAutoProxyCreator app = new DefaultAdvisorAutoProxyCreator();
// 强制使用cglib,防止重复代理和可能引起代理出错的问题
app.setProxyTargetClass(true);
return app;
}
@Bean("connectionFactory")
public JedisConnectionFactory connectionFactory() {
String redisHost = "127.0.0.1";
int redisPort = 6379;
//redis配置单节点
RedisStandaloneConfiguration redisStandaloneConfiguration = new RedisStandaloneConfiguration(redisHost,redisPort);
return new JedisConnectionFactory(redisStandaloneConfiguration);
}
@Bean(name = "redisTemplate")
public RedisTemplate<Serializable, Session> redisTemplate(){
RedisTemplate<Serializable, Session> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(connectionFactory());
//设置redis的String/value的默认序列化方式
GenericJackson2JsonRedisSerializer jackson2JsonRedisSerializer = new GenericJackson2JsonRedisSerializer();
redisTemplate.setKeySerializer(jackson2JsonRedisSerializer);
redisTemplate.setHashKeySerializer(jackson2JsonRedisSerializer);
redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer);
redisTemplate.afterPropertiesSet();
return redisTemplate;
}
@Bean(name = "redisCacheManager")
@DependsOn(value = "redisTemplate")
public RedisCacheManager redisCacheManager(){
return new RedisCacheManager(redisTemplate());
}
}
根据ShiroConfig中FilterChainDefinitionMap的配置,/login 请求是不会被 jwtFilter 过滤器拦截的。Shiro验证用户名和密码正确后完成登录,同时生成 JWT token 签名,并随Response一起返回
@Controller
public class UserJwtController {
@Autowired
private UserService userService;
@RequestMapping(value = "/login",method = RequestMethod.POST)
@ResponseBody
public String login(String username, String password, HttpServletResponse response) {
// 获取当前登录用户
Subject subject = SecurityUtils.getSubject();
boolean loginSuccess = false;
try {
subject.login(new UsernamePasswordToken(username, password));
if (subject.isAuthenticated()){
loginSuccess = true;
}
} catch (UnknownAccountException e) {
e.printStackTrace();
System.out.println("用户名错误~");
} catch (IncorrectCredentialsException e) {
e.printStackTrace();
System.out.println("密码错误~");
} catch (Exception e) {
e.printStackTrace();
}
if (loginSuccess) {
// 若登录成功,签发 JWT token
String jwtToken = JwtUtils.sign(username, JwtUtils.SECRET);
// 将签发的 JWT token 设置到 HttpServletResponse 的 Header 中
((HttpServletResponse) response).setHeader(JwtUtils.AUTH_HEADER, jwtToken);
return "登录成功:"+jwtToken;
}else {
return "登录失败";
}
}
}
ArticleController有两个示例接口:访问 /article/delete 需要 admin 角色;访问 /article/read 需要 order:*:*
权限。
@RestController
@RequestMapping("/article")
public class ArticleController {
@GetMapping("/delete")
@RequiresRoles(value = { "admin" })
public String deleteArticle(int id) {
System.out.println("1111111111111111"+id);
return "文章删除成功!";
}
@GetMapping("/read")
@RequiresPermissions(value = { "order:*:*" })
public String readArticle(int id) {
System.out.println("1111111111111111"+id);
return "请您鉴赏";
}
@GetMapping("/find")
@RequiresPermissions(value = { "order:find:*" })
public String findArticle(int id) {
System.out.println("1111111111111111"+id);
return "请您鉴赏2222";
}
}
package com.mye.hl21shrio.utils;
import java.util.Calendar;
import java.util.Date;
import java.util.Map;
import java.util.Map.Entry;
import org.apache.shiro.crypto.SecureRandomNumberGenerator;
import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTCreator.Builder;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.JWTCreationException;
import com.auth0.jwt.exceptions.JWTDecodeException;
import com.auth0.jwt.exceptions.JWTVerificationException;
import com.auth0.jwt.interfaces.Claim;
import com.auth0.jwt.interfaces.DecodedJWT;
public class JwtUtils {
// 过期时间5分钟
private static final long EXPIRE_TIME = 5 * 60 * 1000;
// 私钥
public static final String SECRET = "SECRET_VALUE";
// 请求头
public static final String AUTH_HEADER = "Authorization";
/** * 验证token是否正确 */
public static boolean verify(String token, String username, String secret) {
try {
Algorithm algorithm = Algorithm.HMAC256(secret);
JWTVerifier verifier = JWT.require(algorithm).withClaim("username", username).build();
verifier.verify(token);
return true;
} catch (JWTVerificationException exception) {
return false;
}
}
/** * 获得token中的自定义信息,无需secret解密也能获得 */
public static String getClaimFiled(String token, String filed) {
try {
DecodedJWT jwt = JWT.decode(token);
return jwt.getClaim(filed).asString();
} catch (JWTDecodeException e) {
return null;
}
}
/** * 生成签名 */
public static String sign(String username, String secret) {
try {
Date date = new Date(System.currentTimeMillis() + EXPIRE_TIME);
Algorithm algorithm = Algorithm.HMAC256(secret);
// 附带username,nickname信息
return JWT.create().withClaim("username", username).withExpiresAt(date).sign(algorithm);
} catch (JWTCreationException e) {
return null;
}
}
/** * 获取 token的签发时间 */
public static Date getIssuedAt(String token) {
try {
DecodedJWT jwt = JWT.decode(token);
return jwt.getIssuedAt();
} catch (JWTDecodeException e) {
return null;
}
}
/** * 验证 token是否过期 */
public static boolean isTokenExpired(String token) {
Date now = Calendar.getInstance().getTime();
DecodedJWT jwt = JWT.decode(token);
return jwt.getExpiresAt().before(now);
}
/** * 刷新 token的过期时间 */
public static String refreshTokenExpired(String token, String secret) {
DecodedJWT jwt = JWT.decode(token);
Map<String, Claim> claims = jwt.getClaims();
try {
Date date = new Date(System.currentTimeMillis() + EXPIRE_TIME);
Algorithm algorithm = Algorithm.HMAC256(secret);
Builder builer = JWT.create().withExpiresAt(date);
for (Entry<String, Claim> entry : claims.entrySet()) {
builer.withClaim(entry.getKey(), entry.getValue().asString());
}
// 附带username,nickname信息
return builer.sign(algorithm);
} catch (JWTCreationException e) {
return null;
}
}
/** * 生成16位随机盐 */
public static String generateSalt() {
SecureRandomNumberGenerator secureRandom = new SecureRandomNumberGenerator();
String hex = secureRandom.nextBytes(16).toHex();
return hex;
}
}
package com.mye.hl21shrio.shiro.token;
import com.mye.hl21shrio.utils.JwtUtils;
import org.apache.shiro.authc.AuthenticationToken;
public class JwtToken implements AuthenticationToken {
private static final long serialVersionUID = 1L;
// 加密后的 JWT token串
private String token;
private String userName;
public JwtToken(String token) {
this.token = token;
this.userName = JwtUtils.getClaimFiled(token, "username");
}
@Override
public Object getPrincipal() {
return this.userName;
}
@Override
public Object getCredentials() {
return token;
}
}
/** * JwtRealm 只负责校验 JwtToken */
public class JwtRealm extends AuthorizingRealm {
/** * 限定这个 Realm 只处理我们自定义的 JwtToken */
@Override
public boolean supports(AuthenticationToken token) {
return token instanceof JwtToken;
}
/** * 认证: * 此处的 SimpleAuthenticationInfo 可返回任意值,密码校验时不会用到它 */
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authcToken) throws AuthenticationException {
JwtToken jwtToken = (JwtToken) authcToken;
if (jwtToken.getPrincipal() == null) {
throw new AccountException("JWT token参数异常!");
}
// 从 JwtToken 中获取当前用户
String username = jwtToken.getPrincipal().toString();
// 查询数据库获取用户信息,
// 由于CustomerRealm并没有交由工厂管理,故不能诸如UserService
UserService userService = (UserService) ApplicationContextUtil.getBean("userServiceImpl");
User user = userService.findUserByUserName(username);
// 用户不存在
if (user == null) {
throw new UnknownAccountException("用户不存在!");
}
return new SimpleAuthenticationInfo(
user.getUsername(),
user.getPassword(),
new ShiroByteSource(user.getSalt()),
this.getName());
}
/** * 授权 */
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
// 获取主身份信息
String principal = (String) principals.getPrimaryPrincipal();
// 根据主身份信息获取角色信息
UserService userService = (UserService) ApplicationContextUtil.getBean("userServiceImpl");
User user = userService.findUserByUserName(principal);
// 查询数据库,获取用户的角色信息
RoleService roleService = (RoleService) ApplicationContextUtil.getBean("roleServiceImpl");
List<Role> roles = roleService.getRolesByUserId(user.getId());
// 查询数据库,获取用户的权限信息
if(!CollectionUtils.isEmpty(roles)){
SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
roles.forEach(role -> {
simpleAuthorizationInfo.addRole(role.getName());
PermissionService permissionService = (PermissionService) ApplicationContextUtil.getBean("permissionServiceImpl");
List<Permission> permissions = permissionService.getPermissionsByRoleId(role.getId());
if(!CollectionUtils.isEmpty(permissions)){
permissions.forEach(permission -> {
simpleAuthorizationInfo.addStringPermission(permission.getName());
});
}
});
return simpleAuthorizationInfo;
}
return null;
}
}
package com.mye.hl21shrio.shiro.realm;
import com.mye.hl21shrio.entity.Permission;
import com.mye.hl21shrio.entity.Role;
import com.mye.hl21shrio.entity.User;
import com.mye.hl21shrio.service.PermissionService;
import com.mye.hl21shrio.service.RoleService;
import com.mye.hl21shrio.service.UserService;
import com.mye.hl21shrio.shiro.cache.ShiroByteSource;
import com.mye.hl21shrio.utils.ApplicationContextUtil;
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.CollectionUtils;
import org.springframework.util.ObjectUtils;
import java.util.List;
/** * 自定义realm */
public class DbShiroRealm extends AuthorizingRealm {
/** * 限定这个 Realm 只处理 UsernamePasswordToken */
@Override
public boolean supports(AuthenticationToken token) {
return token instanceof UsernamePasswordToken;
}
// 授权
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
// 获取主身份信息
String principal = (String) principalCollection.getPrimaryPrincipal();
// 根据主身份信息获取角色信息
UserService userService = (UserService) ApplicationContextUtil.getBean("userServiceImpl");
User user = userService.findUserByUserName(principal);
RoleService roleService = (RoleService) ApplicationContextUtil.getBean("roleServiceImpl");
List<Role> roles = roleService.getRolesByUserId(user.getId());
if(!CollectionUtils.isEmpty(roles)){
SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
roles.forEach(role -> {
simpleAuthorizationInfo.addRole(role.getName());
PermissionService permissionService = (PermissionService) ApplicationContextUtil.getBean("permissionServiceImpl");
List<Permission> permissions = permissionService.getPermissionsByRoleId(role.getId());
if(!CollectionUtils.isEmpty(permissions)){
permissions.forEach(permission -> {
simpleAuthorizationInfo.addStringPermission(permission.getName());
});
}
});
return simpleAuthorizationInfo;
}
return null;
}
// 认证
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
// 在token中获取用户名
String principal = (String) token.getPrincipal();
// 由于CustomerRealm并没有交由工厂管理,故不能诸如UserService
UserService userService = (UserService) ApplicationContextUtil.getBean("userServiceImpl");
User user = userService.findUserByUserName(principal);
if(!ObjectUtils.isEmpty(user)){
return new SimpleAuthenticationInfo(
user.getUsername(),
user.getPassword(),
new ShiroByteSource(user.getSalt()),
this.getName());
}
return null;
}
}
根据ShiroConfig中FilterChainDefinitionMap的配置,/article/delete 和 /article/read 两个请求都会被 jwtFilter 过滤器拦截。
jwtFilter 先检查请求头中是否包含 JWT token,若不包含直接拒绝访问。反之,jwtFilter 会将请求头中包含的 JWT token 封装成 JwtToken 对象,并调用 subject.login(token) 方法交给Shiro去进行登录判断。
package com.mye.hl21shrio.shiro.filter;
import java.io.PrintWriter;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import com.mye.hl21shrio.shiro.token.JwtToken;
import com.mye.hl21shrio.utils.JwtUtils;
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.BasicHttpAuthenticationFilter;
import org.apache.shiro.web.util.WebUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.RequestMethod;
/** * 自定义的认证过滤器,用来拦截Header中携带 JWT token的请求 */
public class JwtFilter extends BasicHttpAuthenticationFilter {
private Logger log = LoggerFactory.getLogger(this.getClass());
/** * 前置处理 */
@Override
protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {
HttpServletRequest httpServletRequest = WebUtils.toHttp(request);
HttpServletResponse httpServletResponse = WebUtils.toHttp(response);
// 跨域时会首先发送一个option请求,这里我们给option请求直接返回正常状态
if (httpServletRequest.getMethod().equals(RequestMethod.OPTIONS.name())) {
httpServletResponse.setStatus(HttpStatus.OK.value());
return false;
}
return super.preHandle(request, response);
}
/** * 后置处理 */
@Override
protected void postHandle(ServletRequest request, ServletResponse response) {
// 添加跨域支持
this.fillCorsHeader(WebUtils.toHttp(request), WebUtils.toHttp(response));
}
/** * 过滤器拦截请求的入口方法 * 返回 true 则允许访问 * 返回false 则禁止访问,会进入 onAccessDenied() */
@Override
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
// 原用来判断是否是登录请求,在本例中不会拦截登录请求,用来检测Header中是否包含 JWT token 字段
if (this.isLoginRequest(request, response)) {
return false;
}
boolean allowed = false;
try {
// 检测Header里的 JWT token内容是否正确,尝试使用 token进行登录
allowed = executeLogin(request, response);
} catch (IllegalStateException e) { // not found any token
log.error("Not found any token");
} catch (Exception e) {
log.error("Error occurs when login", e);
}
return allowed || super.isPermissive(mappedValue);
}
/** * 检测Header中是否包含 JWT token 字段 */
@Override
protected boolean isLoginAttempt(ServletRequest request, ServletResponse response) {
return ((HttpServletRequest) request).getHeader(JwtUtils.AUTH_HEADER) == null;
}
/** * 身份验证,检查 JWT token 是否合法 */
@Override
protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception {
AuthenticationToken token = createToken(request, response);
if (token == null) {
String msg = "createToken method implementation returned null. A valid non-null AuthenticationToken "
+ "must be created in order to execute a login attempt.";
throw new IllegalStateException(msg);
}
try {
Subject subject = getSubject(request, response);
subject.login(token); // 交给 Shiro 去进行登录验证
return onLoginSuccess(token, subject, request, response);
} catch (AuthenticationException e) {
return onLoginFailure(token, e, request, response);
}
}
/** * 从 Header 里提取 JWT token */
@Override
protected AuthenticationToken createToken(ServletRequest servletRequest, ServletResponse servletResponse) {
HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequest;
String authorization = httpServletRequest.getHeader(JwtUtils.AUTH_HEADER);
JwtToken token = new JwtToken(authorization);
return token;
}
/** * isAccessAllowed()方法返回false,会进入该方法,表示拒绝访问 */
@Override
protected boolean onAccessDenied(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception {
HttpServletResponse httpResponse = WebUtils.toHttp(servletResponse);
httpResponse.setCharacterEncoding("UTF-8");
httpResponse.setContentType("application/json;charset=UTF-8");
httpResponse.setStatus(HttpStatus.UNAUTHORIZED.value());
PrintWriter writer = httpResponse.getWriter();
writer.write("{\"errCode\": 401, \"msg\": \"UNAUTHORIZED\"}");
fillCorsHeader(WebUtils.toHttp(servletRequest), httpResponse);
return false;
}
/** * Shiro 利用 JWT token 登录成功,会进入该方法 */
@Override
protected boolean onLoginSuccess(AuthenticationToken token, Subject subject, ServletRequest request,
ServletResponse response) throws Exception {
HttpServletResponse httpResponse = WebUtils.toHttp(response);
String newToken = null;
if (token instanceof JwtToken) {
newToken = JwtUtils.refreshTokenExpired(token.getCredentials().toString(), JwtUtils.SECRET);
}
if (newToken != null) {
httpResponse.setHeader(JwtUtils.AUTH_HEADER, newToken);
}
return true;
}
/** * Shiro 利用 JWT token 登录失败,会进入该方法 */
@Override
protected boolean onLoginFailure(AuthenticationToken token, AuthenticationException e, ServletRequest request,
ServletResponse response) {
// 此处直接返回 false ,交给后面的 onAccessDenied()方法进行处理
return false;
}
/** * 添加跨域支持 */
protected void fillCorsHeader(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse) {
httpServletResponse.setHeader("Access-control-Allow-Origin", httpServletRequest.getHeader("Origin"));
httpServletResponse.setHeader("Access-Control-Allow-Methods", "GET,POST,OPTIONS,HEAD");
httpServletResponse.setHeader("Access-Control-Allow-Headers",
httpServletRequest.getHeader("Access-Control-Request-Headers"));
}
}
跟 ShiroRealm 不一样,JwtRealm 不需要拿传入的 JwtToken 和其他的 Token 去做比对,只需验证JwtToken自身的内容是否合法即可。所以,我们需要为 JwtRealm 自定义一个 CredentialsMatcher 实现。
package com.mye.hl21shrio.shiro.filter;
import com.mye.hl21shrio.utils.JwtUtils;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.credential.CredentialsMatcher;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.JWTVerificationException;
public class JwtCredentialsMatcher implements CredentialsMatcher {
private final Logger logger = LoggerFactory.getLogger(this.getClass());
/** * JwtCredentialsMatcher只需验证JwtToken内容是否合法 */
@Override
public boolean doCredentialsMatch(AuthenticationToken authenticationToken, AuthenticationInfo authenticationInfo) {
String token = authenticationToken.getCredentials().toString();
String username = authenticationToken.getPrincipal().toString();
try {
Algorithm algorithm = Algorithm.HMAC256(JwtUtils.SECRET);
JWTVerifier verifier = JWT.require(algorithm).withClaim("username", username).build();
verifier.verify(token);
return true;
} catch (JWTVerificationException e) {
logger.error(e.getMessage());
}
return false;
}
}
MultiRealmAuthenticator 用来解决Shiro中出现的具体的认证异常无法正常返回,仅返回父类 AuthenticationException 的问题。
package com.mye.hl21shrio.shiro.Authenticator;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.pam.AuthenticationStrategy;
import org.apache.shiro.authc.pam.ModularRealmAuthenticator;
import org.apache.shiro.realm.Realm;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Collection;
/** * 自定义认证器,解决 Shiro 异常无法返回的问题 */
public class MultiRealmAuthenticator extends ModularRealmAuthenticator {
private static final Logger log = LoggerFactory.getLogger(MultiRealmAuthenticator.class);
@Override
protected AuthenticationInfo doMultiRealmAuthentication(Collection<Realm> realms, AuthenticationToken token)
throws AuthenticationException {
AuthenticationStrategy strategy = getAuthenticationStrategy();
AuthenticationInfo aggregate = strategy.beforeAllAttempts(realms, token);
if (log.isTraceEnabled()) {
log.trace("Iterating through {} realms for PAM authentication", realms.size());
}
AuthenticationException authenticationException = null;
for (Realm realm : realms) {
aggregate = strategy.beforeAttempt(realm, token, aggregate);
if (realm.supports(token)) {
log.trace("Attempting to authenticate token [{}] using realm [{}]", token, realm);
AuthenticationInfo info = null;
try {
info = realm.getAuthenticationInfo(token);
} catch (AuthenticationException e) {
authenticationException = e;
if (log.isDebugEnabled()) {
String msg = "Realm [" + realm
+ "] threw an exception during a multi-realm authentication attempt:";
log.debug(msg, e);
}
}
aggregate = strategy.afterAttempt(realm, token, info, aggregate, authenticationException);
} else {
log.debug("Realm [{}] does not support token {}. Skipping realm.", realm, token);
}
}
if (authenticationException != null) {
throw authenticationException;
}
aggregate = strategy.afterAllAttempts(token, aggregate);
return aggregate;
}
}
输入正确的用户名和密码,登录成功。
删除文章
使用zhangsan账号登录,可以删除文章。
阅读文章
使用lisi账号登录,不具备删除文章权限。
版权说明 : 本文为转载文章, 版权归原作者所有 版权申明
原文链接 : https://blog.csdn.net/weixin_43296313/article/details/121164415
内容来源于网络,如有侵权,请联系作者删除!