本章节基于: 六、Springboot 整合Shiro---03权限控制
Nutz 可以做什么?
- Dao -- 针对 JDBC 的薄封装,事务模板,无缓存
- Ioc -- JSON 风格的配置文件,声明时切片支持
- Mvc -- 注解风格的配置,内置多文件上传功能
- Json -- 解析和渲染
- Castors -- Java 对象类型转换
- Lang -- 更简洁的 Java 函数以及更丰富的反射支持
- Aop -- 轻便快速的切面编程支持
- Resource -- 资源扫描
它所有的功能均不强制依赖第三方 jar 包
这就意味着:
- 如果一个 Web 应用,你在 WEB-INF/lib 下只 需要放置一个 nutz.jar 就够了
- 当然你要使用连接池,数据库驱动, websocket等功能,还需要自行添置 jar 包。
支持的环境
- JDK5+, 推荐JDK8
- 任意SQL数据库,例如MySQL,Oracle,SqlServer等等
- 任意支持servlet 2.5的web容器, 推荐Tomcat 8.5+/Jetty 9.2+
Nutz 为谁而设计?
- 如果你觉得 Hibernate 控制比较繁琐,iBatis 编写SQL又比较麻烦,Nutz.Dao 专为你设计。
- 如果你觉得在多个服务器部署或者修改 Spring 配置文件很麻烦,Nutz.Ioc 专为你设计
- 如果你觉得直接写 XML 配置文件很麻烦,可视化编辑器又没控制感,Nutz.Mvc 专为你设计
- 如果你觉得 JSON 转换很麻烦(要写超过一行以上的代码),Nutz.Json 专为你设计
- 如果你觉得 Java 语法不如 Ruby 便捷, Nutz.Castor 以及 Nutz.Lang 专为你设计
- 如果你以前根本没接触过 SSH ,只使用 JDBC 编程, 整个 Nutz 专门为你设计
Nutz 的质量
截至到现在为止,Nutz 的 JUnit 用例覆盖率大概是这样的
并且这个数字还在不断增加。
在一个功能告一段落以后,我通常会花1-2个晚上在一边咂着廉价的红酒一边颇有成就感的书写JUnit测试。 通常我会用 JUnit 把我自己击溃,紧接着的那几天我都努力让那个该死红条变绿,之后,又想方设法写出 新的Junit测试试图让它再度变红。并且我还要保证所做的修改不能让代码膨胀,这的确让我死掉了不少脑 细胞。这些测试中,不仅涵盖各种功能上的测试,也涵盖了一些跨越线程的测试。在以后,我会针对代码执 行的效率加入一些新的测试。
Springboot整合Nutz(这里只用到nutz的Dao):
一、在pom文件中引入依赖:
<!--Springboot整合Nutz-->
<dependency>
<groupId>org.nutz</groupId>
<artifactId>nutz-plugins-spring-boot-starter</artifactId>
<version>1.r.66</version>
</dependency>
<!--阿里的数据连接池-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.9</version>
</dependency>
<!--数据库驱动-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
二、在application.yml中配置数据库链接信息和nutz
server:
#设置程序启动端口号
port: 7000
beetl:
#模板路径
templatesPath: templates
oauth:
qq:
#你的appid
client_id: 123456
#你的appkey
client_secret: aaaaaaaaa
#你接收响应code码地址
redirect_uri: http://localhost:7000/authorize/qq
#腾讯获取code码地址
code_callback_uri: https://graph.qq.com/oauth2.0/authorize
#腾讯获取access_token地址
access_token_callback_uri: https://graph.qq.com/oauth2.0/token
#腾讯获取openid地址
openid_callback_uri: https://graph.qq.com/oauth2.0/me
#腾讯获取用户信息地址
user_info_callback_uri: https://graph.qq.com/user/get_user_info
spring:
aop:
#开启aop代理
auto: true
proxy-target-class: true
datasource:
driver-class-name: com.mysql.jdbc.Driver
username: root
password: 123456
url: jdbc:mysql://127.0.0.1:3306/example?characterEncoding=utf8&autoReconnect=true&failOverReadOnly=false&maxReconnects=10&useSSL=false
type: com.alibaba.druid.pool.DruidDataSource
druid:
# 初始连接数
initial-size: 5
# 最大激活数
max-active: 50
# 最大等待时间
max-wait: 3000
# 是否启用非公平锁
use-unfair-lock: true
# mysql 用 false | oracle 用 true
pool-prepared-statements: false
nutz:
json:
auto-unicode: false
quote-name: true
ignore-null: true
null-as-emtry: true
enabled: true
mode: compact
dao:
runtime:
create: true #自动创建表
migration: false #根据bena自动更新表结构
basepackage: com.xslde.model.mapped #扫描bean
sqlmanager:
paths:
- sqls #sql文件存放位置
三、创建三个实体类外加一个工具类:
1、用户:
package com.xslde.model.mapped;
import org.nutz.dao.entity.annotation.*;
import java.io.Serializable;
import java.util.List;
/**
* @Author xslde
* @Description
* @Date 2018/7/20 16:23
*/
@Table("xslde_user")//批量见表时候,扫描到该注解会创建一个表名称为”s_user“的表
public class User implements Serializable {
@Id
private Integer id;
//用户名称
@Column//该注解表示,在nutz创建表时会创建该字段
@Comment("用户名称")//这个注解是数据库表中对应字段的说明
private String username;
@Column
@Comment("昵称")
private String nickname;
//用户密码
@Column
private String password;
//密码加盐
@Column
private String salt;
//用户是否可用
@Column
private Integer available;
@ManyMany(relation = "xslde_user_roles",from = "user_id",to = "role_id")
private List<Role> roles;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getNickname() {
return nickname;
}
public void setNickname(String nickname) {
this.nickname = nickname;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public String getSalt() {
return salt;
}
public void setSalt(String salt) {
this.salt = salt;
}
public Integer getAvailable() {
return available;
}
public void setAvailable(Integer available) {
this.available = available;
}
public List<Role> getRoles() {
return roles;
}
public void setRoles(List<Role> roles) {
this.roles = roles;
}
}
2、角色:
package com.xslde.model.mapped;
import org.nutz.dao.entity.annotation.*;
import java.io.Serializable;
import java.util.List;
/**
* Created by xslde on 2018/7/23
*/
@Table("xslde_role")
public class Role implements Serializable {
@Id
private Integer id;
@Column
private String role;
@Column
private String describes;
@ManyMany(relation = "xslde_role_permis",from = "role_id",to = "permis_id")
private List<Permission> permissions;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getRole() {
return role;
}
public void setRole(String role) {
this.role = role;
}
public String getDescribes() {
return describes;
}
public void setDescribes(String describes) {
this.describes = describes;
}
public List<Permission> getPermissions() {
return permissions;
}
public void setPermissions(List<Permission> permissions) {
this.permissions = permissions;
}
}
3、权限:
package com.xslde.model.mapped;
import org.nutz.dao.entity.annotation.Id;
import org.nutz.dao.entity.annotation.Table;
import java.io.Serializable;
/**
* Created by xslde on 2018/7/23
*/
@Table("xslde_permission")
public class Permission implements Serializable {
@Id
private Integer id;
//角色
private String permission;
//描述
private String describes;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getPermission() {
return permission;
}
public void setPermission(String permission) {
this.permission = permission;
}
public String getDescribes() {
return describes;
}
public void setDescribes(String describes) {
this.describes = describes;
}
}
4、密码加密工具类:
package com.xslde.utils;
import org.apache.shiro.crypto.hash.SimpleHash;
/**
* Created by xslde on 2018/7/23
*/
public class PasswordUtils {
/**
*
* @param salt 盐
* @param password 明文密码
* @return
*/
public static String getPassword(String salt,String password){
String hashAlgorithmName = "md5";//加密类型
Integer iteration = 2;//迭代次数
return new SimpleHash(hashAlgorithmName,password,salt,iteration).toHex();
}
}
四、对LoginAction.class改动:
//Nutz dao
@Autowired
Dao dao;
//接收回调地址带过来的code码
@GetMapping("/authorize/qq")
public String authorizeQQ(Map<String, String> msg, String code) {
HashMap<String, Object> params = new HashMap<>();
params.put("code", code);
params.put("grant_type", "authorization_code");
params.put("redirect_uri", oauth.getQQ().getRedirect_uri());
params.put("client_id", oauth.getQQ().getClient_id());
params.put("client_secret", oauth.getQQ().getClient_secret());
//获取access_token如:access_token=9724892714FDF1E3ED5A4C6D074AF9CB&expires_in=7776000&refresh_token=9E0DE422742ACCAB629A54B3BFEC61FF
//TODO 其实整合NUTZ后,可以不使用HttpUtils工具类,并去掉pom中httpasyncclient、httpmime依赖,使用NUTZ自带HTTP工具
String result = HttpsUtils.doGet(oauth.getQQ().getAccess_token_callback_uri(), params);
//对拿到的数据进行切割字符串
String[] strings = result.split("&");
//切割好后放进map
Map<String, String> reulsts = new HashMap<>();
for (String str : strings) {
String[] split = str.split("=");
if (split.length > 1) {
reulsts.put(split[0], split[1]);
}
}
//到这里access_token已经处理好了
//下一步获取openid,只有拿到openid才能拿到用户信息
String openidContent = HttpsUtils.doGet(oauth.getQQ().getOpenid_callback_uri() + "?access_token=" + reulsts.get("access_token"));
//接下来对openid进行处理
//截取需要的那部分json字符串
String openid = openidContent.substring(openidContent.indexOf("{"), openidContent.indexOf("}") + 1);
Gson gson = new Gson();
//将返回的openid转换成DTO
QQOpenidDTO qqOpenidDTO = gson.fromJson(openid, QQOpenidDTO.class);
User existUser = dao.fetch(User.class, Cnd.where("username", "=", qqOpenidDTO.getOpenid()));
if (existUser == null) {
params.clear();
params.put("access_token", reulsts.get("access_token"));//设置access_token
params.put("openid", qqOpenidDTO.getOpenid());//设置openid
params.put("oauth_consumer_key", qqOpenidDTO.getClient_id());//设置appid
String userInfo = HttpsUtils.doGet(oauth.getQQ().getUser_info_callback_uri(), params);
QQDTO qqDTO = gson.fromJson(userInfo, QQDTO.class);//json转换成dto
User user = new User();
String salt = UUID.randomUUID().toString();
user.setNickname(qqDTO.getNickname());
user.setUsername(qqOpenidDTO.getOpenid());
user.setPassword(PasswordUtils.getPassword(salt,qqOpenidDTO.getOpenid()));
user.setSalt(salt);
user.setAvailable(1);
User insert = dao.insert(user);//向数据库插入数据
ArrayList<Role> roles = new ArrayList<>();
Role role = new Role();
role.setId(1);//id = 1 是普通用户
roles.add(role);
insert.setRoles(roles);//将角色集合存入用户实体类
dao.insertRelation(insert,"roles");//将用户和角色关联起来
return QQLogin(msg, qqOpenidDTO);
} else {
return QQLogin(msg, qqOpenidDTO);
}
//这里拿用户昵称,作为用户名,openid作为密码(正常情况下,在开发时候用openid作为用户名,再自己定义个密码就可以了)
}
private String QQLogin(Map<String, String> msg, QQOpenidDTO qqOpenidDTO) {
try {
SecurityUtils.getSubject().login(new UsernamePasswordToken(qqOpenidDTO.getOpenid(), qqOpenidDTO.getOpenid()));
} catch (Exception e) {
msg.put("msg", "第三方登陆失败,请联系管理!");
logger.error(e.getMessage());
return "login.html";
}
return "redirect:/index";
}
五、对ShiroRealm.class改动:
package com.xslde.configurer;
import com.xslde.model.mapped.Permission;
import com.xslde.model.mapped.Role;
import com.xslde.model.mapped.User;
import org.apache.shiro.authc.*;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.crypto.hash.SimpleHash;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.util.ByteSource;
import org.nutz.dao.Cnd;
import org.nutz.dao.Dao;
import org.nutz.json.Json;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import java.util.List;
import java.util.UUID;
/**
* @Author xslde
* @Description
* @Date 2018/7/20 16:30
*/
public class ShiroRealm extends AuthorizingRealm {
private Logger logger = LoggerFactory.getLogger(getClass());
@Autowired
Dao dao;
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
//给当前用户授权的权限(功能权限、角色)
SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
//xslde用户拥有user角色
User user = (User) principals.getPrimaryPrincipal();
List<Role> roles = user.getRoles();
if (roles!=null&&roles.size()>0){
//遍历所有用户拥有的角色添加到授权信息中
for (Role role:roles){
authorizationInfo.addRole(role.getRole());
//查询角色拥有的所有权限,并重新复制给角色
role = dao.fetchLinks(role,"permissions");
logger.info("角色详情"+Json.toJson(role));
if (role.getPermissions()!=null&&role.getPermissions().size()>0){
//遍历所有角色拥有的权限添加到授权信息中
for (Permission permis:role.getPermissions()){
authorizationInfo.addStringPermission(permis.getPermission());
}
}
}
}
return authorizationInfo;
}
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
//获取用户名
String username = (String) token.getPrincipal();
//根据用户名查询用户信息和用户角色
User existUser = dao.fetchLinks(dao.fetch(User.class, Cnd.where("username", "=", username)),"roles");
logger.info("用户详情"+Json.toJson(existUser));
if (existUser==null){
throw new UnknownAccountException("用户不存在!");
}
if (existUser.getAvailable()!=1){
throw new LockedAccountException("账户已被锁定");
}
return new SimpleAuthenticationInfo(existUser, existUser.getPassword(), ByteSource.Util.bytes(existUser.getSalt()), getName());
}
//生成一个加盐密码
public static void main(String[] args) {
/* String hashAlgorithmName = "md5";//加密类型
Integer iteration = 2;//迭代次数
String password = "123456";
String salt = "abcd";
String s = new SimpleHash(hashAlgorithmName,password,salt,iteration).toHex();
System.out.println(s);
//加密后的密码*/
//0caf568dbf30f5c33a13c56b869259fc
for (int i=0;i<=10;i++){
System.out.println(UUID.randomUUID().toString().replace("-",""));
}
}
}
5、配置好数据库、启动项目完成自动建表成功,执行下面sql命令:
INSERT INTO `xslde_user`(`id`, `username`, `nickname`, `password`, `salt`, `available`) VALUES (1, 'xslde', 'xslde', '0caf568dbf30f5c33a13c56b869259fc', 'abcd', 1);
INSERT INTO `xslde_user`(`id`, `username`, `nickname`, `password`, `salt`, `available`) VALUES (2, 'admin', '超级管理员', '0caf568dbf30f5c33a13c56b869259fc', 'abcd', 1);
INSERT INTO `xslde_role`(`id`, `role`, `describes`) VALUES (1, 'user', '普通用户');
INSERT INTO `xslde_role`(`id`, `role`, `describes`) VALUES (2, 'admin', '超级管理员');
INSERT INTO `xslde_permission`(`id`, `permission`, `describes`) VALUES (1, 'user:query', '普通用户查看权限');
INSERT INTO `xslde_permission`(`id`, `permission`, `describes`) VALUES (2, 'admin:delete', '超级管理员删除权限');
INSERT INTO `xslde_user_roles`(`user_id`, `role_id`) VALUES ('1', '1');
INSERT INTO `xslde_user_roles`(`user_id`, `role_id`) VALUES ('2', '2');
INSERT INTO `xslde_role_permis`(`role_id`, `permis_id`) VALUES ('1', '1');
INSERT INTO `xslde_role_permis`(`role_id`, `permis_id`) VALUES ('2', '2');
6、打开浏览器访问:http://localhost:7000/login,用xslde和admin用户登录就会进入数据库查询,如果是第三方QQ登录,第一次登录会插入第三方登录用户信息,之后的每次登录,都是通过openid查询。
数据库已存在用户数据,登录后台打印sql执行如下:
当第一次访问受权限保护资源时候,后台打印执行sql如下:
项目地址:springboot-example07