主要实现用户的认证,授权,鉴权三个功能
微服务架构:开发工具IDEA
采用eureka作为注册中心,SpringBoot/Cloud+shiro+前端模板(thymeleaf)+mysql5.7
eureka注册中心服务请参照eureka创建全过程(idea)
整合步骤如下:新建一个module工程
- pom文件
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!--SpringBoot集成shiro导的依赖-->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.3.2</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.16.22</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
<version>1.5.21.RELEASE</version>
</dependency>
- shiro目录结构如图
注意:这里你只需要两个服务就行了 一个是eureka-server(注册中心),另一个shiro-server(权限服务)
shio-server的启动类上别忘了加一个注解:@EnableEurekaClient(该服务注册到eureka上)
- application.yml配置如下
#shiro服务端口
server:
port: 7777
#shiro服务名称
spring:
application:
name: shiro-server
#关联更新数据库,自动建表update:有变动的字段就更新,不存在表就建表
jpa:
hibernate:
ddl-auto: update
#jdbc4个基本配置 驱动/url/密码/用户名
datasource:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://localhost:3306/microservices?characterEncoding=utf-8&useSSL=true
password: root
username: root
#关闭缓存
thymeleaf:
cache: false
#静态html文件存储位置,默认templates目录下
prefix: classpath:/templates/
#默认后缀为html,之后的html转发无需加后缀html
suffix: .html
#该服务IP地址
eureka:
instance:
hostname: localhost
#注册到注册中心的地址
client:
service-url:
defaultZone: http://localhost:8761/eureka/
#控制台打印日志信息
logging:
level:
root: INFO
org.hibernate: INFO
org.hibernate.type.descriptor.sql.BasicBinder: TRACE
org.hibernate.type.descriptor.sql.BasicExtractor: TRACE
com.springms: DEBUG
- templates目录下html文件代码
/**
**登录页面
**/
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.w3.org/1999/xhtml">
<head>
<meta charset="UTF-8"/>
<title>Title</title>
</head>
<body>
<font color="red" th:text="${message}"></font>
<form action="/loginFrom" method="post">
用户名<input type="text" name="username" /><br/>
密码<input type="password" name="password"/><br/>
<input type="submit" value="登录"/>
</form>
</body>
</html>
/**
**登录成功页面
**/
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>登录成功</title>
</head>
<body>
<a href="/find">查看数据页面</a><br/>
<a href="/delete">删除数据页面</a>
</body>
</html>
/**
**数据页面
**/
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8"/>
<title>Title</title>
</head>
<body>
<h1>查看数据页面</h1>
</body>
</html>
/**
**如果该用户没有权限,跳转页面
**/
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8"/>
<title>Title</title>
</head>
<body>
<h1>未授权页面展示</h1>
</body>
</html>
/**
**删除页面
**/
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8"/>
<title>删除页面</title>
</head>
<body>
<h1>删除页面展示</h1>
</body>
</html>
- shiro服务启动类代码
@SpringBootApplication
@EnableEurekaClient
public class ShiroServerApplication {
public static void main(String[] args) {
SpringApplication.run(ShiroServerApplication.class, args);
}
}
- mysql数据库3个表创建代码
/**
**用户表users
**/
CREATE TABLE `users` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '编号',
`username` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '用户名',
`password` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '密码',
`salt` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '盐值',
`role_id` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '角色列表',
`locked` tinyint(1) NULL DEFAULT 0 COMMENT '是否锁定',
PRIMARY KEY (`id`) USING BTREE,
UNIQUE INDEX `idx_sys_users_username`(`username`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 2 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- 为用户表插入一条数据
-- ----------------------------
INSERT INTO `users` VALUES (1, 'mark', '6d295738eb6579053ac46a9ca1902583', NULL, NULL, 0);
/**
**角色表user_roles
**/
CREATE TABLE `user_roles` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '角色编号',
`username` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '角色名称',
`role_name` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '角色描述',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 2 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- 为角色表插入一条数据
-- ----------------------------
INSERT INTO `user_roles` VALUES (1, 'mark', 'admin');
/**
**权限表roles_permissions
**/
CREATE TABLE `roles_permissions` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '编号',
`role_name` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '权限编号',
`permission` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '权限描述',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 4 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
-- ----------------------------
--为权限表插入一条数据
-- ----------------------------
INSERT INTO `roles_permissions` VALUES (1, 'admin', 'user:select');
INSERT INTO `roles_permissions` VALUES (2, 'admin', 'user:add');
- Controller接收前端请求,并负责认证,鉴权~
@Controller
public class LoginController {
/**
* shiro认证登录
*
* @param username
* @param password
* @return
*/
@PostMapping(value = "/loginFrom")
public String login(String username, String password,Model model) {
try {
//shiro Token处理类
UsernamePasswordToken token = new UsernamePasswordToken(username, password);
//获得提交认证类
Subject subject = SecurityUtils.getSubject();
subject.login(token);
} catch (UnknownAccountException e){
model.addAttribute("message",e.getMessage());
return "login";
} catch (IncorrectCredentialsException e) {
model.addAttribute("message","用户名或密码错误!");
return "login";
} catch (AuthenticationException e) {
model.addAttribute("message",e.getMessage());
return "login";
}
return "success";
}
/**
* 跳转登录页
*
* @return
*/
@RequestMapping(value = "/login")
public String goToLogin() {
return "login";
}
/**
* 查看数据页面
* @return
*/
@RequestMapping(value = "/find")
public String goToFind() {
return "find";
}
/**
* 查看数据页面
* @return
*/
@RequestMapping(value = "/delete")
public String goToDelete() {
return "delete";
}
/**
* 跳转成功页面
* @return
*/
@RequestMapping(value = "/success")
public String goTosuccess() {
return "success";
}
/**
* 未授权(也就是没有权限的页面)
* @return
*/
@RequestMapping(value = "/noauth")
public String goTonoauth() {
return "noauth";
}
}
- SpringBoot核心拦截器配置
@Configuration
public class ShiroConfig {
/**
* 配置shiro web过滤器, 拦截/转发请求
*/
@Bean
public ShiroFilterFactoryBean shiroWebConfigFilter() {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
//配置拦截链 LinkedHashMap有序,shiro会根据添加的顺序进行拦截
// Map<K,V> K指的是拦截的url V值的是该url是否拦截
//添加securityManager环境
//将shiro环境添加过滤器链~
shiroFilterFactoryBean.setSecurityManager(securityManager());
Map<String, String> filterChainMap = new LinkedHashMap<>();
//authc:所有url都必须认证通过才可以访问; anon:所有url都都可以匿名访问,先配置anon再配置authc。
filterChainMap.put("/loginFrom", "anon");
filterChainMap.put("/login", "anon");
filterChainMap.put("/success", "anon");
/**
* 添加授权过滤器 验证当前用户是否有删除权限,如果没有跳转未授权页面或抛出异常信息
*/
filterChainMap.put("/delete", "perms[user:delete]");
filterChainMap.put("/**", "authc");
//拦截所有请求直接到登录页面
shiroFilterFactoryBean.setLoginUrl("/login");
shiroFilterFactoryBean.setUnauthorizedUrl("/noauth");
//对某些资源进行拦截处理
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainMap);
return shiroFilterFactoryBean;
}
/**
* 构建secrityManager环境
*
* @return
*/
@Bean
public SecurityManager securityManager() {
DefaultWebSecurityManager securityManager = null;
securityManager = new DefaultWebSecurityManager();
//将自定义realm注入环境中去
CustomRealm customRealm = new CustomRealm();
securityManager.setRealm(customRealm);
//shiro加密~在前端传入密码进后端后对密码进行加密
HashedCredentialsMatcher matcher = new HashedCredentialsMatcher();
matcher.setHashAlgorithmName("md5");
customRealm.setCredentialsMatcher(matcher);
return securityManager;
}
}
- 自定义Realm
/**
* 自定义realm
*/
@SuppressWarnings("all")
@Component
public class CustomRealm extends AuthorizingRealm {
@Autowired
private JdbcTemplate jdbcTemplate;
@Autowired
private DataSource dataSource;
public static CustomRealm customRealm;
/**
* 初始化普通类之前 将数据源和JDBC模板赋值给当前Bean
* 之后将该Bean添加SpringBoot管理
* SpringBoot好像普通类在运行时自动注入都会找不到,原因应该是Springboot的某种特性吧
*/
@PostConstruct
public void inits(){
customRealm=this;
customRealm.dataSource=this.dataSource;
customRealm.jdbcTemplate=this.jdbcTemplate;
}
//校验该用户有哪些权限
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
String userName = (String) principalCollection.getPrimaryPrincipal();
//获取角色数据
Set<String> users = getRolesByUserName(userName);
Set<String> permisss = getPermissionByUserName(userName);
SimpleAuthorizationInfo so = new SimpleAuthorizationInfo();
so.setStringPermissions(permisss);
so.setRoles(users);
return so;
}
//该角色有什么权限
private Set<String> getPermissionByUserName(String userName) {
Set<String> rolesName = new HashSet<String>();
ResultSet resultSet = null;
String roleSql = "SELECT permission FROM\n" +
"roles_permissions WHERE role_name = (select role_name from user_roles where username =" + "'" + userName + "')";
try {
customRealm.jdbcTemplate.setDataSource(customRealm.dataSource);
List<String> permission = customRealm.jdbcTemplate.query(roleSql, new RowMapper<String>() {
@Override
public String mapRow(ResultSet resultSet, int i) throws SQLException {
return resultSet.getString("permission");
}
});
rolesName.addAll(permission);
return rolesName;
} catch (Exception e) {
throw new AuthenticationException("获得角色权限失败");
}
}
//用户属于什么角色
private Set<String> getRolesByUserName(String userName) {
Set<String> rolesName = new HashSet<String>();
String roleSql = "select role_name from user_roles where username = " + "'" + userName + "'";
try {
customRealm.jdbcTemplate.setDataSource(customRealm.dataSource);
List<String> role_name = customRealm.jdbcTemplate.query(roleSql, new RowMapper<String>() {
@Override
public String mapRow(ResultSet resultSet, int i) throws SQLException {
return resultSet.getString("role_name");
}
});
rolesName.addAll(role_name);
} catch (Exception e) {
throw new AuthenticationException("获得用户的角色失败");
}
return rolesName;
}
//校验用户名密码是否正确
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
//获得用户输入账号
String username = (String) authenticationToken.getPrincipal();
//数据库校验
String password = getPasswordByUserName(username);
//如果密码为空 则用户不存在
//如果不为空 SimpleAuthenticationInfo做身份验证处理
//加盐加密
//如果用户名不存在shiro底层会抛出UnknownAccountException
if (StringUtils.isEmpty(password)) {
throw new UnknownAccountException("用户名不存在");
}
SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo(username, password, "CustomRealm");
simpleAuthenticationInfo.setCredentialsSalt(ByteSource.Util.bytes(username));
return simpleAuthenticationInfo;
}
//根据用户名获得密码
@SuppressWarnings("all")
private String getPasswordByUserName(String username) {
String sql = "select password from users where username = " + "'" + username + "'";
String password = null;
try {
customRealm.jdbcTemplate.setDataSource(customRealm.dataSource);
password = customRealm.jdbcTemplate.queryForObject(sql, String.class);
//jdbcTemplate 在查询字符串是null时会抛出异常
} catch (EmptyResultDataAccessException e) {
throw new AuthenticationException("用户名不存在");
}
return password;
}
}
自此搭建完成里面核心代码我都有注释,建议细致的了解shiro的执行过程,如果有疑问或者问题加我Q或V
QQ:2509647976 微信:x331191249
- 演示效果图
该实例只解决了简单的认证,鉴权,后续会结合jwt+oauth2进行权限控制,继续努力。