概念莱文斯坦距离(Levenshtein distance)是编辑距离的一种。它由俄罗斯科学家 弗拉基米尔·莱文斯坦在1965年提出。两个字符串之间的莱文斯坦距离是将一个单词更改为另一个单词所需的最少编辑操作次数。莱文斯坦距离允许的编辑操作包括单个字符的 替换/插入/删除。示例"Saturday" 和 "Sundays" 之间的 Levenshtein 距离是 4 1.) Saturday → Sturday // 删除第一个 a 2.) Sturday → Surday // 删除第一个 t 3.) Surday → Sunday // 替换 r 为 n 4.) Sunday → Sundays // 结尾添加 s计算方法莱文斯坦距离本质上可以理解为一个动态规划问题，其状态转移方程为：解释一下状态转移方程：方程第一行：如果 min(i,j) = 0（即当前两个字符串至少有一个长度为0，即是空字符串）时，则此时的莱文斯坦距离就是两字符串的长度的最大值。这个很好理解，不赘述了方程第二行：如果min(i,j) != 0，则取如下三个数字的最小值：2.1. 将a字符串的长度i减小1，计算其与长度为j的b字符串的莱文斯坦距离，并将其结果再加1，这其实等价于将当前a字符串的i位置的字符删除了；2.2. 将b字符串的长度j减小1，计算其与长度为i的a字符串的莱文斯坦距离，并将其结果再加1，这其实等价于在当前a字符串的i位置后面插入了一个字符（即插入了b字符串j位置的那个字符）；2.3. 将str_a、str_b字符串的长度同时减小1，计算两者的剩余字符串的莱文斯坦距离，此时如果str_a的i位置的字符和str_b的j位置的字符不同，则结果再加1，这其实等价于将str_a的i位置的字符改为str_b的j位置的那个字符；计算过程Java代码public class LevenshteinUtil { /** * 编辑距离算法，（用于计算距离相近的字符串） * * @param word1 * @param word2 * @return */ public static int minDistance(String word1, String word2) { int n = word1.length(); int m = word2.length(); if (n * m == 0) return n + m; int[][] d = new int[n + 1][m + 1]; for (int i = 0; i < n + 1; i++) { d[i][0] = i; } for (int j = 0; j < m + 1; j++) { d[0][j] = j; } for (int i = 1; i < n + 1; i++) { for (int j = 1; j < m + 1; j++) { int left = d[i - 1][j] + 1; int down = d[i][j - 1] + 1; int left_down = d[i - 1][j - 1]; if (word1.charAt(i - 1) != word2.charAt(j - 1)) left_down += 1; d[i][j] = Math.min(left, Math.min(down, left_down)); } } return d[n][m]; } /** * 编辑距离算法，（用于计算距离相近的标签） * * @param tagList1 * @param tagList2 * @return */ public static int minDistance(List<String> tagList1, List<String> tagList2) { int n = tagList1.size(); int m = tagList2.size(); if (n * m == 0) return n + m; int[][] d = new int[n + 1][m + 1]; for (int i = 0; i < n + 1; i++) { d[i][0] = i; } for (int j = 0; j < m + 1; j++) { d[0][j] = j; } for (int i = 1; i < n + 1; i++) { for (int j = 1; j < m + 1; j++) { int left = d[i - 1][j] + 1; int down = d[i][j - 1] + 1; int left_down = d[i - 1][j - 1]; if (!Objects.equals(tagList1.get(i - 1), tagList2.get(j - 1))) left_down += 1; d[i][j] = Math.min(left, Math.min(down, left_down)); } } return d[n][m]; } public static int minDistanceByLabel(List<Label> tagList1, List<Label> tagList2) { int n = tagList1.size(); int m = tagList2.size(); if (n * m == 0) return n + m; int[][] d = new int[n + 1][m + 1]; for (int i = 0; i < n + 1; i++) { d[i][0] = i; } for (int j = 0; j < m + 1; j++) { d[0][j] = j; } for (int i = 1; i < n + 1; i++) { for (int j = 1; j < m + 1; j++) { int left = d[i - 1][j] + 1; int down = d[i][j - 1] + 1; int left_down = d[i - 1][j - 1]; if (!Objects.equals(tagList1.get(i - 1).getName(), tagList2.get(j - 1).getName())) left_down += 1; d[i][j] = Math.min(left, Math.min(down, left_down)); } } return d[n][m]; } }
FastDFS分布式文件系统之防盗链权限控制
问题所在FastDFS采用的是tracker和storage的模式。tracker负责负载均衡storage负责文件存储、文件同步、提供文件访问接口（也就是说用户要从storage直接获取文件）其实，storage的http端口是开着的，外界只要知道端口和文件路径信息就可以直接访问到文件。这种操作，对于文件而言是不安全的。解决方式在storage上配置访问规则，打开防盗链功能，首先要确保不能直接访问到文件。其次，限制访问文件时必须带有一个本地应用服务器生成的token，这个token需要有文件id、自己设置的密钥、时间戳三个部分组成。但是只有本地应用服务器保存密钥是不可行的，需要把密钥同时保存到storage上，用于token的解密验证工作。配置storage进入etc/fdfs，编辑http.conf文件编辑后的内容Javaresources下新建fdfs_cilent.conf文件#默认值为30s connect_timeout = 10 #默认值为30s network_timeout = 30 charset = UTF-8 #tracker的http端口 http.tracker_http_port = 8888 #服务器 tracker_server = 192.168.1.106:22122 #开启防盗链，防盗链密钥 http.anti_steal_token = true http.secret_key= 密钥主要的工具类文件/** * 上传下载等文件操作工具类 */ public class FastDFSClient { private static org.slf4j.Logger logger = LoggerFactory.getLogger(FastDFSClient.class); /*** * 初始化加载FastDFS的TrackerServer配置 */ static { try { String filePath = new ClassPathResource("fdfs_client.conf").getFile().getAbsolutePath(); ClientGlobal.init(filePath); } catch (Exception e) { logger.error("FastDFS Client Init Fail!",e); } } public static void main(String[] args) throws UnsupportedEncodingException, NoSuchAlgorithmException, MyException { String fileName = "M00/00/00/wKgBa2U4dPSAO7fXAAC5PC_5iKw01.docx"; String groupName = "group1"; String sourceUrl = getSourceUrl(groupName,fileName); System.out.println(sourceUrl); } public static FastDFSFile getFastDFSFile(MultipartFile file,String author) { InputStream inputStream = null; //获取文件名称（带有后缀） String allFileName = file.getOriginalFilename(); //获取文件扩展名 String ext = ""; byte[] content = null; if (allFileName != null && !"".equals(allFileName)) { ext = (allFileName.substring(allFileName.lastIndexOf("."))).replaceAll("\\.",""); } //文件名称（不带后缀） String fileName = allFileName.replaceAll("." + ext, ""); try { inputStream = file.getInputStream(); //获取文件所有的字节内容 content = inputStream.readAllBytes(); //一定要关闭输入流，不然会报错 inputStream.close(); } catch (IOException e) { throw new CustomException("文件上传错误", ResultCode.UPLOAD_ERROR); } //创建文件实体类对象 FastDFSFile fastDFSFile = new FastDFSFile(); fastDFSFile.setContent(content); fastDFSFile.setSize(content.length); fastDFSFile.setName(fileName); fastDFSFile.setExt(ext); fastDFSFile.setAuthor(author); return fastDFSFile; } /*** * 文件上传 * @param file * @return */ public static String[] upload(FastDFSFile file) { //获取文件基础信息,有多少长度就要是多少，不然会报空指针 NameValuePair[] meta_list = new NameValuePair[4]; meta_list[0] = new NameValuePair("author", file.getAuthor()); meta_list[1] = new NameValuePair("name",file.getName()); meta_list[2] = new NameValuePair("ext",file.getExt()); meta_list[3] = new NameValuePair("size", file.getSize()+""); //接收返回数据 String[] uploadResults = null; StorageClient storageClient=null; try { //创建StorageClient客户端对象 storageClient = getTrackerClient(); /*** * 文件上传 * 1)文件字节数组 * 2)文件扩展名 * 3)文件作者 */ uploadResults = storageClient.upload_file(file.getContent(), file.getExt(), meta_list); } catch (Exception e) { logger.error("Exception when uploadind the file:" + file.getName(), e); } if (uploadResults == null && storageClient!=null) { logger.error("upload file fail, error code:" + storageClient.getErrorCode()); } //获取组名 String groupName = uploadResults[0]; //获取文件存储路径 String remoteFileName = uploadResults[1]; return uploadResults; } /*** * 获取文件信息 * @param groupName:组名 * @param remoteFileName：文件存储完整名 * @return */ public static FileInfo getFile(String groupName, String remoteFileName) { try { StorageClient storageClient = getTrackerClient(); return storageClient.get_file_info(groupName, remoteFileName); } catch (Exception e) { logger.error("Exception: Get File from Fast DFS failed", e); } return null; } /*** * 文件下载 * @param groupName * @param remoteFileName * @return */ public static InputStream downFile(String groupName, String remoteFileName) { try { //创建StorageClient StorageClient storageClient = getTrackerClient(); //下载文件 byte[] fileByte = storageClient.download_file(groupName, remoteFileName); InputStream ins = new ByteArrayInputStream(fileByte); return ins; } catch (Exception e) { logger.error("Exception: Get File from Fast DFS failed", e); } return null; } /*** * 文件删除 * @param groupName * @param remoteFileName * @throws Exception */ public static void deleteFile(String groupName, String remoteFileName) throws Exception { //创建StorageClient StorageClient storageClient = getTrackerClient(); //删除文件 int i = storageClient.delete_file(groupName, remoteFileName); } /*** * 获取Storage客户端 * @return * @throws IOException */ private static StorageClient getTrackerClient() throws IOException { TrackerServer trackerServer = getTrackerServer(); StorageClient storageClient = new StorageClient(trackerServer, null); return storageClient; } /*** * 获取Tracker * @return * @throws IOException */ private static TrackerServer getTrackerServer() throws IOException { TrackerClient trackerClient = new TrackerClient(); TrackerServer trackerServer = trackerClient.getConnection(); return trackerServer; } /** * 生成防盗链token * @param groupName 文件组名称，group1 * @param fileName 文件路径，不带group：M00/00/00/wKg4C1tFmTWAFPKBAADdeFFxlXA240.png * @return * @throws UnsupportedEncodingException * @throws NoSuchAlgorithmException * @throws MyException */ public static String getSourceUrl(String groupName,String fileName) throws UnsupportedEncodingException, NoSuchAlgorithmException, MyException { //时间戳 int lts = (int)(System.currentTimeMillis() / 1000); System.out.println(ClientGlobal.getG_secret_key()); System.out.println("ip:"+FastDFSClient.getFile(groupName, fileName).getSourceIpAddr()); //ip自动获取 String ip = FastDFSClient.getFile(groupName, fileName).getSourceIpAddr(); String token = ProtoCommon.getToken(fileName, lts, ClientGlobal.getG_secret_key()); //初始化secret_key //生成防盗链的文件访问url return "http://" +ip+ ":8888/" +groupName+"/" + fileName + "?token=" + token + "&ts=" + lts; } /*** * 获取Storage组 * @param groupName * @return * @throws IOException */ public static StorageServer[] getStoreStorages(String groupName) throws IOException { //创建TrackerClient TrackerClient trackerClient = new TrackerClient(); //获取TrackerServer TrackerServer trackerServer = trackerClient.getConnection(); //获取Storage组 return trackerClient.getStoreStorages(trackerServer, groupName); } /*** * 获取Storage信息,IP和端口 * @param groupName * @param remoteFileName * @return * @throws IOException */ public static ServerInfo[] getFetchStorages(String groupName,String remoteFileName) throws IOException { TrackerClient trackerClient = new TrackerClient(); TrackerServer trackerServer = trackerClient.getConnection(); return trackerClient.getFetchStorages(trackerServer, groupName, remoteFileName); } /*** * 获取Tracker服务地址 * @return * @throws IOException */ public static String getTrackerUrl() throws IOException { return "http://"+getTrackerServer().getInetSocketAddress().getHostString()+":"+ ClientGlobal.getG_tracker_http_port()+"/"; } } 附属的文件类/** * 文件实体类 */ @Data @AllArgsConstructor @NoArgsConstructor public class FastDFSFile { //文件名字 private String name; //文件内容 private byte[] content; //文件扩展名 private String ext; //文件MD5摘要值 private String md5; //文件创建作者 private String author; //文件大小 private Integer size; public FastDFSFile(String name, byte[] content, String ext) { super(); this.name = name; this.content = content; this.ext = ext; } } 参考资料https://www.jianshu.com/p/bd077cdc2c82https://www.cnblogs.com/fwfjava/p/17674975.html
授权，也叫访问控制，即在应用中控制谁能访问哪些资源（如访问页面/编辑数据/页面操作等）。在授权中需了解的几个关键对象：主体（Subject）、资源（Resource）、权限（Permission）、角色（Role） 主体主体，即访问应用的用户，在Shiro中使用Subject代表该用户。用户只有授权后才允许访问相应的资源。资源在应用中用户可以访问的任何东西，比如访问JSP页面、查看/编辑某些数据、访问某个业务方法、打印文本等等都是资源。用户只要授权后才能访问。权限安全策略中的原子授权单位，通过权限我们可以表示在应用中用户有没有操作某个资源的权力。即权限表示在应用中用户能不能访问某个资源，如：访问用户列表页面查看/新增/修改/删除用户数据（即很多时候都是CRUD（增查改删）式权限控制）打印文档等等。。。如上可以看出，权限代表了用户有没有操作某个资源的权利，即反映在某个资源上的操作允不允许，不反映谁去执行这个操作。所以后续还需要把权限赋予给用户，即定义哪个用户允许在某个资源上做什么操作（权限），Shiro不会去做这件事情，而是由实现人员提供。Shiro支持粗粒度权限（如用户模块的所有权限）和细粒度权限（操作某个用户的权限，即实例级别的），后续部分介绍。角色角色代表了操作集合，可以理解为权限的集合，一般情况下我们会赋予用户角色而不是权限，即这样用户可以拥有一组权限，赋予权限时比较方便。典型的如：项目经理、技术总监、CTO、开发工程师等都是角色，不同的角色拥有一组不同的权限。隐式角色：即直接通过角色来验证用户有没有操作权限，如在应用中CTO、技术总监、开发工程师可以使用打印机，假设某天不允许开发工程师使用打印机，此时需要从应用中删除相应代码；再如在应用中CTO、技术总监可以查看用户、查看权限；突然有一天不允许技术总监查看用户、查看权限了，需要在相关代码中把技术总监角色从判断逻辑中删除掉；即粒度是以角色为单位进行访问控制的，粒度较粗；如果进行修改可能造成多处代码修改。显示角色：在程序中通过权限控制谁能访问某个资源，角色聚合一组权限集合；这样假设哪个角色不能访问某个资源，只需要从角色代表的权限集合中移除即可；无须修改多处代码；即粒度是以资源/实例为单位的；粒度较细。请google搜索“RBAC”和“RBAC新解”分别了解“基于角色的访问控制”“基于资源的访问控制(Resource-Based Access Control)”。授权方式Shiro支持三种方式的授权：编程式：通过写if/else授权代码块完成：Subject subject = SecurityUtils.getSubject(); if(subject.hasRole(“admin”)) { //有权限 } else { //无权限 }注解式：通过在执行的Java方法上放置相应的注解完成：@RequiresRoles("admin") public void hello() { //有权限 } 没有权限将抛出相应的异常；JSP/GSP标签：在JSP/GSP页面通过相应的标签完成：<shiro:hasRole name="admin"> <!— 有权限 —></shiro:hasRole> 后续部分将详细介绍如何使用。 授权基于角色的访问控制（隐式角色）1、在ini配置文件配置用户拥有的角色（shiro-role.ini）[users] zhang=123,role1,role2 wang=123,role1规则即：“用户名=密码,角色1，角色2”，如果需要在应用中判断用户是否有相应角色，就需要在相应的Realm中返回角色信息，也就是说Shiro不负责维护用户-角色信息，需要应用提供，Shiro只是提供相应的接口方便验证，后续会介绍如何动态的获取用户角色。2、测试用例（com.github.zhangkaitao.shiro.chapter3.RoleTest）@Test public void testHasRole() { login("classpath:shiro-role.ini", "zhang", "123"); //判断拥有角色：role1 Assert.assertTrue(subject().hasRole("role1")); //判断拥有角色：role1 and role2 Assert.assertTrue(subject().hasAllRoles(Arrays.asList("role1", "role2"))); //判断拥有角色：role1 and role2 and !role3 boolean[] result = subject().hasRoles(Arrays.asList("role1", "role2", "role3")); Assert.assertEquals(true, result[0]); Assert.assertEquals(true, result[1]); Assert.assertEquals(false, result[2]); } Shiro提供了hasRole/hasRole用于判断用户是否拥有某个角色/某些权限；但是没有提供如hashAnyRole用于判断是否有某些权限中的某一个。@Test(expected = UnauthorizedException.class) public void testCheckRole() { login("classpath:shiro-role.ini", "zhang", "123"); //断言拥有角色：role1 subject().checkRole("role1"); //断言拥有角色：role1 and role3 失败抛出异常 subject().checkRoles("role1", "role3"); }Shiro提供的checkRole/checkRoles和hasRole/hasAllRoles不同的地方是它在判断为假的情况下会抛出UnauthorizedException异常。到此基于角色的访问控制（即隐式角色）就完成了，这种方式的缺点就是如果很多地方进行了角色判断，但是有一天不需要了那么就需要修改相应代码把所有相关的地方进行删除；这就是粗粒度造成的问题。基于资源的访问控制（显示角色）1、在ini配置文件配置用户拥有的角色及角色-权限关系（shiro-permission.ini）[users] zhang=123,role1,role2 wang=123,role1 [roles] role1=user:create,user:update role2=user:create,user:delete 规则：“用户名=密码，角色1，角色2”“角色=权限1，权限2”，即首先根据用户名找到角色，然后根据角色再找到权限；即角色是权限集合；Shiro同样不进行权限的维护，需要我们通过Realm返回相应的权限信息。只需要维护“用户——角色”之间的关系即可。2、测试用例（com.github.zhangkaitao.shiro.chapter3.PermissionTest）@Test public void testIsPermitted() { login("classpath:shiro-permission.ini", "zhang", "123"); //判断拥有权限：user:create Assert.assertTrue(subject().isPermitted("user:create")); //判断拥有权限：user:update and user:delete Assert.assertTrue(subject().isPermittedAll("user:update", "user:delete")); //判断没有权限：user:view Assert.assertFalse(subject().isPermitted("user:view")); } Shiro提供了isPermitted和isPermittedAll用于判断用户是否拥有某个权限或所有权限，也没有提供如isPermittedAny用于判断拥有某一个权限的接口。@Test(expected = UnauthorizedExceptionclass) public void testCheckPermission () { login("classpath:shiro-permissionini", "zhang", "123"); //断言拥有权限：user:create subject().checkPermission("user:create"); //断言拥有权限：user:delete and user:update subject().checkPermissions("user:delete", "user:update"); //断言拥有权限：user:view 失败抛出异常 subject().checkPermissions("user:view"); } 但是失败的情况下会抛出UnauthorizedException异常。到此基于资源的访问控制（显示角色）就完成了，也可以叫基于权限的访问控制，这种方式的一般规则是“资源标识符：操作”，即是资源级别的粒度；这种方式的好处就是如果要修改基本都是一个资源级别的修改，不会对其他模块代码产生影响，粒度小。但是实现起来可能稍微复杂点，需要维护“用户——角色，角色——权限（资源：操作）”之间的关系。 Permission字符串通配符权限规则：“资源标识符：操作：对象实例ID” 即对哪个资源的哪个实例可以进行什么操作。其默认支持通配符权限字符串，“:”表示资源/操作/实例的分割；“,”表示操作的分割；“*”表示任意资源/操作/实例。1、单个资源单个权限subject().checkPermissions("system:user:update");用户拥有资源“system:user”的“update”权限。2、单个资源多个权限ini配置文件role41=system:user:update,system:user:delete然后通过如下代码判断subject().checkPermissions("system:user:update", "system:user:delete");用户拥有资源“system:user”的“update”和“delete”权限。如上可以简写成：ini配置（表示角色4拥有system:user资源的update和delete权限）role42="system:user:update,delete"接着可以通过如下代码判断subject().checkPermissions("system:user:update,delete");通过“system:user:update,delete”验证"system:user:update, system:user:delete"是没问题的，但是反过来是规则不成立。3、单个资源全部权限ini配置role51="system:user:create,update,delete,view"然后通过如下代码判断subject().checkPermissions("system:user:create,delete,update:view");用户拥有资源“system:user”的“create”、“update”、“delete”和“view”所有权限。如上可以简写成：ini配置文件（表示角色5拥有system:user的所有权限）role52=system:user:*也可以简写为（推荐上边的写法）：role53=system:user然后通过如下代码判断subject().checkPermissions("system:user:*"); subject().checkPermissions("system:user"); 通过“system:user:*”验证“system:user:create,delete,update:view”可以，但是反过来是不成立的。4、所有资源全部权限ini配置role61=*:view然后通过如下代码判断subject().checkPermissions("user:view"); 用户拥有所有资源的“view”所有权限。假设判断的权限是“"system:user:view”，那么需要“role5=::view”这样写才行。5、实例级别的权限5.1、单个实例单个权限ini配置role71=user:view:1对资源user的1实例拥有view权限。然后通过如下代码判断 subject().checkPermissions("user:view:1");5.2、单个实例多个权限ini配置 role72="user:update,delete:1"对资源user的1实例拥有update、delete权限。然后通过如下代码判断subject().checkPermissions("user:delete,update:1"); subject().checkPermissions("user:update:1", "user:delete:1");5.3、单个实例所有权限ini配置 role73=user:*:1对资源user的1实例拥有所有权限。然后通过如下代码判断 subject().checkPermissions("user:update:1", "user:delete:1", "user:view:1");5.4、所有实例单个权限ini配置 role74=user:auth:*对资源user的1实例拥有所有权限。然后通过如下代码判断 subject().checkPermissions("user:auth:1", "user:auth:2");5.5、所有实例所有权限ini配置 role75=user::对资源user的1实例拥有所有权限。然后通过如下代码判断 subject().checkPermissions("user:view:1", "user:auth:2");6、Shiro对权限字符串缺失部分的处理如“user:view”等价于“user:view:”；而“organization”等价于“organization:”或者“organization::”。可以这么理解，这种方式实现了前缀匹配。另外如“user:”可以匹配如“user:delete”、“user:delete”可以匹配如“user:delete:1”、“user::1”可以匹配如“user:view:1”、“user”可以匹配“user:view”或“user:view:1”等。即可以匹配所有，不加可以进行前缀匹配；但是如“:view”不能匹配“system:user:view”，需要使用“::view”，即后缀匹配必须指定前缀（多个冒号就需要多个来匹配）。7、WildcardPermission如下两种方式是等价的： subject().checkPermission("menu:view:1"); subject().checkPermission(new WildcardPermission("menu:view:1"));因此没什么必要的话使用字符串更方便。8、性能问题通配符匹配方式比字符串相等匹配来说是更复杂的，因此需要花费更长时间，但是一般系统的权限不会太多，且可以配合缓存来提供其性能，如果这样性能还达不到要求我们可以实现位操作算法实现性能更好的权限匹配。另外实例级别的权限验证如果数据量太大也不建议使用，可能造成查询权限及匹配变慢。可以考虑比如在sql查询时加上权限字符串之类的方式在查询时就完成了权限匹配。 授权流程流程如下：1、首先调用Subject.isPermitted/hasRole接口，其会委托给SecurityManager，而SecurityManager接着会委托给Authorizer；2、Authorizer是真正的授权者，如果我们调用如isPermitted(“user:view”)，其首先会通过PermissionResolver把字符串转换成相应的Permission实例；3、在进行授权之前，其会调用相应的Realm获取Subject相应的角色/权限用于匹配传入的角色/权限；4、Authorizer会判断Realm的角色/权限是否和传入的匹配，如果有多个Realm，会委托给ModularRealmAuthorizer进行循环判断，如果匹配如isPermitted/hasRole会返回true，否则返回false表示授权失败。ModularRealmAuthorizer进行多Realm匹配流程：1、首先检查相应的Realm是否实现了实现了Authorizer；2、如果实现了Authorizer，那么接着调用其相应的isPermitted/hasRole接口进行匹配；3、如果有一个Realm匹配那么将返回true，否则返回false。如果Realm进行授权的话，应该继承AuthorizingRealm，其流程是：1.1、如果调用hasRole*，则直接获取AuthorizationInfo.getRoles()与传入的角色比较即可；1.2、首先如果调用如isPermitted(“user:view”)，首先通过PermissionResolver将权限字符串转换成相应的Permission实例，默认使用WildcardPermissionResolver，即转换为通配符的WildcardPermission；2、通过AuthorizationInfo.getObjectPermissions()得到Permission实例集合；通过AuthorizationInfo. getStringPermissions()得到字符串集合并通过PermissionResolver解析为Permission实例；然后获取用户的角色，并通过RolePermissionResolver解析角色对应的权限集合（默认没有实现，可以自己提供）；3、接着调用Permission. implies(Permission p)逐个与传入的权限比较，如果有匹配的则返回true，否则false。 Authorizer、PermissionResolver及RolePermissionResolverAuthorizer的职责是进行授权（访问控制），是Shiro API中授权核心的入口点，其提供了相应的角色/权限判断接口，具体请参考其Javadoc。SecurityManager继承了Authorizer接口，且提供了ModularRealmAuthorizer用于多Realm时的授权匹配。PermissionResolver用于解析权限字符串到Permission实例，而RolePermissionResolver用于根据角色解析相应的权限集合。我们可以通过如下ini配置更改Authorizer实现：authorizer=org.apache.shiro.authz.ModularRealmAuthorizer securityManager.authorizer=$authorizer对于ModularRealmAuthorizer，相应的AuthorizingSecurityManager会在初始化完成后自动将相应的realm设置进去，我们也可以通过调用其setRealms()方法进行设置。对于实现自己的authorizer可以参考ModularRealmAuthorizer实现即可，在此就不提供示例了。设置ModularRealmAuthorizer的permissionResolver，其会自动设置到相应的Realm上（其实现了PermissionResolverAware接口），如：permissionResolver=org.apache.shiro.authz.permission.WildcardPermissionResolver authorizer.permissionResolver=$permissionResolver设置ModularRealmAuthorizer的rolePermissionResolver，其会自动设置到相应的Realm上（其实现了RolePermissionResolverAware接口），如：rolePermissionResolver=com.github.zhangkaitao.shiro.chapter3.permission.MyRolePermissionResolver authorizer.rolePermissionResolver=$rolePermissionResolver示例1、ini配置（shiro-authorizer.ini） [main] 自定义authorizer authorizer=org.apache.shiro.authz.ModularRealmAuthorizer 自定义permissionResolverpermissionResolver=org.apache.shiro.authz.permission.WildcardPermissionResolver permissionResolver=com.github.zhangkaitao.shiro.chapter3.permission.BitAndWildPermissionResolver authorizer.permissionResolver=$permissionResolver 自定义rolePermissionResolverrolePermissionResolver=com.github.zhangkaitao.shiro.chapter3.permission.MyRolePermissionResolver 1authorizer.rolePermissionResolver=$rolePermissionResolver securityManager.authorizer=$authorizer自定义realm 一定要放在securityManager.authorizer赋值之后（因为调用setRealms会将realms设置给authorizer，并给各个Realm设置permissionResolver和rolePermissionResolver） realm=com.github.zhangkaitao.shiro.chapter3.realm.MyRealm securityManager.realms=$realm 设置securityManager 的realms一定要放到最后，因为在调用SecurityManager.setRealms时会将realms设置给authorizer，并为各个Realm设置permissionResolver和rolePermissionResolver。另外，不能使用IniSecurityManagerFactory创建的IniRealm，因为其初始化顺序的问题可能造成后续的初始化Permission造成影响。2、定义BitAndWildPermissionResolver及BitPermissionBitPermission用于实现位移方式的权限，如规则是：权限字符串格式：+资源字符串+权限位+实例ID；以+开头中间通过+分割；权限：0 表示所有权限；1 新增（二进制：0001）、2 修改（二进制：0010）、4 删除（二进制：0100）、8 查看（二进制：1000）；如 +user+10 表示对资源user拥有修改/查看权限。public class BitPermission implements Permission { private String resourceIdentify; private int permissionBit; private String instanceId; public BitPermission(String permissionString) { String[] array = permissionString.split("\\+"); if(array.length > 1) { resourceIdentify = array[1]; } if(StringUtils.isEmpty(resourceIdentify)) { resourceIdentify = "*"; } if(array.length > 2) { permissionBit = Integer.valueOf(array[2]); } if(array.length > 3) { instanceId = array[3]; } if(StringUtils.isEmpty(instanceId)) { instanceId = "*"; } } @Override public boolean implies(Permission p) { if(!(p instanceof BitPermission)) { return false; } BitPermission other = (BitPermission) p; if(!("*".equals(this.resourceIdentify) || this.resourceIdentify.equals(other.resourceIdentify))) { return false; } if(!(this.permissionBit ==0 || (this.permissionBit & other.permissionBit) != 0)) { return false; } if(!("*".equals(this.instanceId) || this.instanceId.equals(other.instanceId))) { return false; } return true; } } Permission接口提供了boolean implies(Permission p)方法用于判断权限匹配的； public class BitAndWildPermissionResolver implements PermissionResolver { @Override public Permission resolvePermission(String permissionString) { if(permissionString.startsWith("+")) { return new BitPermission(permissionString); } return new WildcardPermission(permissionString); } } BitAndWildPermissionResolver实现了PermissionResolver接口，并根据权限字符串是否以“+”开头来解析权限字符串为BitPermission或WildcardPermission。 3、定义MyRolePermissionResolver RolePermissionResolver用于根据角色字符串来解析得到权限集合。 public class MyRolePermissionResolver implements RolePermissionResolver { @Override public Collection<Permission> resolvePermissionsInRole(String roleString) { if("role1".equals(roleString)) { return Arrays.asList((Permission)new WildcardPermission("menu:*")); } return null; } } 此处的实现很简单，如果用户拥有role1，那么就返回一个“menu:*”的权限。4、自定义Realmpublic class MyRealm extends AuthorizingRealm { @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo(); authorizationInfo.addRole("role1"); authorizationInfo.addRole("role2"); authorizationInfo.addObjectPermission(new BitPermission("+user1+10")); authorizationInfo.addObjectPermission(new WildcardPermission("user1:*")); authorizationInfo.addStringPermission("+user2+10"); authorizationInfo.addStringPermission("user2:*"); return authorizationInfo; } @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { //和com.github.zhangkaitao.shiro.chapter2.realm.MyRealm1. getAuthenticationInfo代码一样，省略 } } 此时我们继承AuthorizingRealm而不是实现Realm接口；推荐使用AuthorizingRealm，因为：AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token)：表示获取身份验证信息；AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals)：表示根据用户身份获取授权信息。这种方式的好处是当只需要身份验证时只需要获取身份验证信息而不需要获取授权信息。对于AuthenticationInfo和AuthorizationInfo请参考其Javadoc获取相关接口信息。另外我们可以使用JdbcRealm，需要做的操作如下：1、执行sql/ shiro-init-data.sql 插入相关的权限数据；2、使用shiro-jdbc-authorizer.ini配置文件，需要设置jdbcRealm.permissionsLookupEnabled为true来开启权限查询。此次还要注意就是不能把我们自定义的如“+user1+10”配置到INI配置文件，即使有IniRealm完成，因为IniRealm在new完成后就会解析这些权限字符串，默认使用了WildcardPermissionResolver完成，即此处是一个设计权限，如果采用生命周期（如使用初始化方法）的方式进行加载就可以解决我们自定义permissionResolver的问题。5、测试用例public class AuthorizerTest extends BaseTest { @Test public void testIsPermitted() { login("classpath:shiro-authorizer.ini", "zhang", "123"); //判断拥有权限：user:create Assert.assertTrue(subject().isPermitted("user1:update")); Assert.assertTrue(subject().isPermitted("user2:update")); //通过二进制位的方式表示权限 Assert.assertTrue(subject().isPermitted("+user1+2"));//新增权限 Assert.assertTrue(subject().isPermitted("+user1+8"));//查看权限 Assert.assertTrue(subject().isPermitted("+user2+10"));//新增及查看 Assert.assertFalse(subject().isPermitted("+user1+4"));//没有删除权限 Assert.assertTrue(subject().isPermitted("menu:view"));//通过MyRolePermissionResolver解析得到的权限 } }
一 shiro的简介shiro是apache的一个开源框架，是一个权限管理的框架，实现 用户认证、用户授权。二 shiro的架构图authenticator:认证器，主体进行认证最终通过authenticator进行authorizer:授权器，主体进行授权最终通过authorizer进行sessionManager:web应用中一般是用web容器对session进行管理，shiro也提供了一套session管理方式SessionDao:通过SessionDao管理session数据，以下是SessionDao的方法public interface SessionDAO {/*如DefaultSessionManager在创建完session后会调用该方法； 如保存到关系数据库/文件系统/NoSQL数据库；即可以实现会话的持久化； 返回会话ID；主要此处返回的ID.equals(session.getId())； */ Serializable create(Session session); //根据会话ID获取会话 Session readSession(Serializable sessionId) throws UnknownSessionException; //更新会话；如更新会话最后访问时间/停止会话/设置超时时间/设置移除属性等会调用 void update(Session session) throws UnknownSessionException; //删除会话；当会话过期/会话停止（如用户退出时）会调用 void delete(Session session); //获取当前所有活跃用户，如果用户量多此方法影响性能 Collection<Session> getActiveSessions();cache Manager：缓存管理器，主要对session和授权数据进行缓存，比如将授权数据通过cacheManager进行缓存管理，和ehcache整合对缓存数据进行管理。三个核心组件:Subject, SecurityManager 和 Realms.Subject:即"当前操作用户"。但是，在Shiro中，Subject这一概念并不仅仅指人，也可以是第三方进程、后台帐户(Daemon Account)或其他类似事物。它仅仅意味着"当前跟软件交互的东西"。但考虑到大多数目的和用途，你可以把它认为是Shiro的"用户"概念。Subject代表了当前用户的安全操作，SecurityManager则管理所有用户的安全操作。SecurityManager:它是Shiro框架的核心，典型的Facade模式，Shiro通过SecurityManager来管理内部组件实例，并通过它来提供安全管理的各种服务。Realm: Realm充当了Shiro与应用安全数据间的"桥梁"或者"连接器"。也就是说，当对用户执行认证(登录)和授权(访问控制)验证时，Shiro会从应用配置的Realm中查找用户及其权限信息。从这个意义上讲，Realm实质上是一个安全相关的DAO:它封装了数据源的连接细节，并在需要时将相关数据提供给Shiro。当配置Shiro时，你必须至少指定一个Realm，用于认证和(或)授权。配置多个Realm是可以的，但是至少需要一个。Shiro内置了可以连接大量安全数据源(又名目录)的Realm，如LDAP、关系数据库(JDBC)、类似INI的文本配置资源以及属性文件等。如果缺省的Realm不能满足需求，你还可以插入代表自定义数据源的自己的Realm实现。三 shiro提供的功能Authentication：用户认证（登录）Authorization：权限控制Session Management：会话管理Cryptography：数据加密Web Support：支持web的APICaching：缓存Concurrency：支持多线程应用程序Testing：测试的支持“Run As”：假设一个用户为另一个用户的身份“Remember Me”：在Session中保存用户身份
一、单系统登录机制1、http无状态协议 web应用采用browser/server架构，http作为通信协议。http是无状态协议，浏览器的每一次请求，服务器会独立处理，不与之前或之后的请求产生关联，这个过程用下图说明，三次请求/响应对之间没有任何联系3c91a3bf-25d8-4b1f-8e4a-68535c51aaa8 但这也同时意味着，任何用户都能通过浏览器访问服务器资源，如果想保护服务器的某些资源，必须限制浏览器请求；要限制浏览器请求，必须鉴别浏览器请求，响应合法请求，忽略非法请求；要鉴别浏览器请求，必须清楚浏览器请求状态。既然http协议无状态，那就让服务器和浏览器共同维护一个状态吧！这就是会话机制2、会话机制 浏览器第一次请求服务器，服务器创建一个会话，并将会话的id作为响应的一部分发送给浏览器，浏览器存储会话id，并在后续第二次和第三次请求中带上会话id，服务器取得请求中的会话id就知道是不是同一个用户了，这个过程用下图说明，后续请求与第一次请求产生了关联8a9fb230-d506-4b19-b821-4001c68c4588 服务器在内存中保存会话对象，浏览器怎么保存会话id呢？你可能会想到两种方式请求参数cookie 将会话id作为每一个请求的参数，服务器接收请求自然能解析参数获得会话id，并借此判断是否来自同一会话，很明显，这种方式不靠谱。那就浏览器自己来维护这个会话id吧，每次发送http请求时浏览器自动发送会话id，cookie机制正好用来做这件事。cookie是浏览器用来存储少量数据的一种机制，数据以”key/value“形式存储，浏览器发送http请求时自动附带cookie信息 tomcat会话机制当然也实现了cookie，访问tomcat服务器时，浏览器中可以看到一个名为“JSESSIONID”的cookie，这就是tomcat会话机制维护的会话id，使用了cookie的请求响应过程如下图518293d9-64b2-459c-9d45-9f353c757d1f3、登录状态 有了会话机制，登录状态就好明白了，我们假设浏览器第一次请求服务器需要输入用户名与密码验证身份，服务器拿到用户名密码去数据库比对，正确的话说明当前持有这个会话的用户是合法用户，应该将这个会话标记为“已授权”或者“已登录”等等之类的状态，既然是会话的状态，自然要保存在会话对象中，tomcat在会话对象中设置登录状态如下用户再次访问时，tomcat在会话对象中查看登录状态HttpSession session = request.getSession();session.setAttribute("isLogin", true);HttpSession session = request.getSession();session.getAttribute("isLogin"); 实现了登录状态的浏览器请求服务器模型如下图描述 每次请求受保护资源时都会检查会话对象中的登录状态，只有 isLogin=true 的会话才能访问，登录机制因此而实现。二、多系统的复杂性 web系统早已从久远的单系统发展成为如今由多系统组成的应用群，面对如此众多的系统，用户难道要一个一个登录、然后一个一个注销吗？就像下图描述的这样6dfbb0b1-46c0-4945-a3bf-5f060fa80710 web系统由单系统发展成多系统组成的应用群，复杂性应该由系统内部承担，而不是用户。无论web系统内部多么复杂，对用户而言，都是一个统一的整体，也就是说，用户访问web系统的整个应用群与访问单个系统一样，登录/注销只要一次就够了9fe14ab3-4254-447b-b850-0436e628c254 虽然单系统的登录解决方案很完美，但对于多系统应用群已经不再适用了，为什么呢？ 单系统登录解决方案的核心是cookie，cookie携带会话id在浏览器与服务器之间维护会话状态。但cookie是有限制的，这个限制就是cookie的域（通常对应网站的域名），浏览器发送http请求时会自动携带与该域匹配的cookie，而不是所有cookie4d58ccfa-0114-486d-bec2-c28f2f9eb513 既然这样，为什么不将web应用群中所有子系统的域名统一在一个顶级域名下，例如“*.baidu.com”，然后将它们的cookie域设置为“baidu.com”，这种做法理论上是可以的，甚至早期很多多系统登录就采用这种同域名共享cookie的方式。 然而，可行并不代表好，共享cookie的方式存在众多局限。首先，应用群域名得统一；其次，应用群各系统使用的技术（至少是web服务器）要相同，不然cookie的key值（tomcat为JSESSIONID）不同，无法维持会话，共享cookie的方式是无法实现跨语言技术平台登录的，比如java、php、.net系统之间；第三，cookie本身不安全。 因此，我们需要一种全新的登录方式来实现多系统应用群的登录，这就是单点登录三、单点登录 什么是单点登录？单点登录全称Single Sign On（以下简称SSO），是指在多系统应用群中登录一个系统，便可在其他所有系统中得到授权而无需再次登录，包括单点登录与单点注销两部分1、登录 相比于单系统登录，sso需要一个独立的认证中心，只有认证中心能接受用户的用户名密码等安全信息，其他系统不提供登录入口，只接受认证中心的间接授权。间接授权通过令牌实现，sso认证中心验证用户的用户名密码没问题，创建授权令牌，在接下来的跳转过程中，授权令牌作为参数发送给各个子系统，子系统拿到令牌，即得到了授权，可以借此创建局部会话，局部会话登录方式与单系统的登录方式相同。这个过程，也就是单点登录的原理，用下图说明 下面对上图简要描述用户访问系统1的受保护资源，系统1发现用户未登录，跳转至sso认证中心，并将自己的地址作为参数sso认证中心发现用户未登录，将用户引导至登录页面用户输入用户名密码提交登录申请sso认证中心校验用户信息，创建用户与sso认证中心之间的会话，称为全局会话，同时创建授权令牌sso认证中心带着令牌跳转会最初的请求地址（系统1）系统1拿到令牌，去sso认证中心校验令牌是否有效sso认证中心校验令牌，返回有效，注册系统1系统1使用该令牌创建与用户的会话，称为局部会话，返回受保护资源用户访问系统2的受保护资源系统2发现用户未登录，跳转至sso认证中心，并将自己的地址作为参数sso认证中心发现用户已登录，跳转回系统2的地址，并附上令牌系统2拿到令牌，去sso认证中心校验令牌是否有效sso认证中心校验令牌，返回有效，注册系统2系统2使用该令牌创建与用户的局部会话，返回受保护资源 用户登录成功之后，会与sso认证中心及各个子系统建立会话，用户与sso认证中心建立的会话称为全局会话，用户与各个子系统建立的会话称为局部会话，局部会话建立之后，用户访问子系统受保护资源将不再通过sso认证中心，全局会话与局部会话有如下约束关系局部会话存在，全局会话一定存在全局会话存在，局部会话不一定存在全局会话销毁，局部会话必须销毁 你可以通过博客园、百度、csdn、淘宝等网站的登录过程加深对单点登录的理解，注意观察登录过程中的跳转url与参数2、注销 单点登录自然也要单点注销，在一个子系统中注销，所有子系统的会话都将被销毁，用下面的图来说明3b139d2e-0b83-4a69-b4f2-316adb8997ce sso认证中心一直监听全局会话的状态，一旦全局会话销毁，监听器将通知所有注册系统执行注销操作 下面对上图简要说明用户向系统1发起注销请求系统1根据用户与系统1建立的会话id拿到令牌，向sso认证中心发起注销请求sso认证中心校验令牌有效，销毁全局会话，同时取出所有用此令牌注册的系统地址sso认证中心向所有注册系统发起注销请求各注册系统接收sso认证中心的注销请求，销毁局部会话sso认证中心引导用户至登录页面四、部署图 单点登录涉及sso认证中心与众子系统，子系统与sso认证中心需要通信以交换令牌、校验令牌及发起注销请求，因而子系统必须集成sso的客户端，sso认证中心则是sso服务端，整个单点登录过程实质是sso客户端与服务端通信的过程，用下图描述fb29685c-487c-42b9-9ceb-6c7ee29e98c9 sso认证中心与sso客户端通信方式有多种，这里以简单好用的httpClient为例，web service、rpc、restful api都可以五、实现 只是简要介绍下基于java的实现过程，不提供完整源码，明白了原理，我相信你们可以自己实现。sso采用客户端/服务端架构，我们先看sso-client与sso-server要实现的功能（下面：sso认证中心=sso-server） sso-client拦截子系统未登录用户请求，跳转至sso认证中心接收并存储sso认证中心发送的令牌与sso-server通信，校验令牌的有效性建立局部会话拦截用户注销请求，向sso认证中心发送注销请求接收sso认证中心发出的注销请求，销毁局部会话 sso-server验证用户的登录信息创建全局会话创建授权令牌与sso-client通信发送令牌校验sso-client令牌有效性系统注册接收sso-client注销请求，注销所有会话 接下来，我们按照原理来一步步实现sso吧！1、sso-client拦截未登录请求 java拦截请求的方式有servlet、filter、listener三种方式，我们采用filter。在sso-client中新建LoginFilter.java类并实现Filter接口，在doFilter()方法中加入对未登录用户的拦截public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest req = (HttpServletRequest) request; HttpServletResponse res = (HttpServletResponse) response; HttpSession session = req.getSession(); if (session.getAttribute("isLogin")) { chain.doFilter(request, response); return; } //跳转至sso认证中心 res.sendRedirect("sso-server-url-with-system-url"); }2、sso-server拦截未登录请求 拦截从sso-client跳转至sso认证中心的未登录请求，跳转至登录页面，这个过程与sso-client完全一样3、sso-server验证用户登录信息 用户在登录页面输入用户名密码，请求登录，sso认证中心校验用户信息，校验成功，将会话状态标记为“已登录”@RequestMapping("/login") public String login(String username, String password, HttpServletRequest req) { this.checkLoginInfo(username, password); req.getSession().setAttribute("isLogin", true); return "success"; }4、sso-server创建授权令牌 授权令牌是一串随机字符，以什么样的方式生成都没有关系，只要不重复、不易伪造即可，下面是一个例子String token = UUID.randomUUID().toString();5、sso-client取得令牌并校验 sso认证中心登录后，跳转回子系统并附上令牌，子系统（sso-client）取得令牌，然后去sso认证中心校验，在LoginFilter.java的doFilter()中添加几行 // 请求附带token参数 String token = req.getParameter("token"); if (token != null) { // 去sso认证中心校验token boolean verifyResult = this.verify("sso-server-verify-url", token); if (!verifyResult) { res.sendRedirect("sso-server-url"); return; } chain.doFilter(request, response); } verify()方法使用httpClient实现，这里仅简略介绍，httpClient详细使用方法请参考官方文档HttpPost httpPost = new HttpPost("sso-server-verify-url-with-token"); HttpResponse httpResponse = httpClient.execute(httpPost);6、sso-server接收并处理校验令牌请求 用户在sso认证中心登录成功后，sso-server创建授权令牌并存储该令牌，所以，sso-server对令牌的校验就是去查找这个令牌是否存在以及是否过期，令牌校验成功后sso-server将发送校验请求的系统注册到sso认证中心（就是存储起来的意思） 令牌与注册系统地址通常存储在key-value数据库（如redis）中，redis可以为key设置有效时间也就是令牌的有效期。redis运行在内存中，速度非常快，正好sso-server不需要持久化任何数据。 令牌与注册系统地址可以用下图描述的结构存储在redis中，可能你会问，为什么要存储这些系统的地址？如果不存储，注销的时候就麻烦了，用户向sso认证中心提交注销请求，sso认证中心注销全局会话，但不知道哪些系统用此全局会话建立了自己的局部会话，也不知道要向哪些子系统发送注销请求注销局部会话3b221593-f9c4-45af-a567-4937786993e87、sso-client校验令牌成功创建局部会话 令牌校验成功后，sso-client将当前局部会话标记为“已登录”，修改LoginFilter.java，添加几行if (verifyResult) { session.setAttribute("isLogin", true); } sso-client还需将当前会话id与令牌绑定，表示这个会话的登录状态与令牌相关，此关系可以用java的hashmap保存，保存的数据用来处理sso认证中心发来的注销请求8、注销过程 用户向子系统发送带有“logout”参数的请求（注销请求），sso-client拦截器拦截该请求，向sso认证中心发起注销请求String logout = req.getParameter("logout"); if (logout != null) { this.ssoServer.logout(token); } sso认证中心也用同样的方式识别出sso-client的请求是注销请求（带有“logout”参数），sso认证中心注销全局会话@RequestMapping("/logout") public String logout(HttpServletRequest req) { HttpSession session = req.getSession(); if (session != null) { session.invalidate();//触发LogoutListener } return "redirect:/"; } sso认证中心有一个全局会话的监听器，一旦全局会话注销，将通知所有注册系统注销public class LogoutListener implements HttpSessionListener { @Override public void sessionCreated(HttpSessionEvent event) {} @Override public void sessionDestroyed(HttpSessionEvent event) { //通过httpClient向所有注册系统发送注销请求 } }
什么是面向切面编程官方：在软件业，AOP为Aspect Oriented Programming的缩写，意为：面向切面编程，通过预编译方式和运行期间动态代理实现程序功能的统一维护的一种技术。AOP是OOP的延续，是软件开发中的一个热点，也是Spring框架中的一个重要内容，是函数式编程的一种衍生范型。利用AOP可以对业务逻辑的各个部分进行隔离，从而使得业务逻辑各部分之间的耦合度降低，提高程序的可重用性，同时提高了开发的效率个人理解：面向切面编程旨在较少冗余代码，使用的是一种代码横向抽取（举例：不同的方法拥有三句同样的代码，那这个代码就可以抽取出来，但使用父类、接口、继承、实现会极大的提高代码的耦合度，因此我们采用面向切面编程，把这三句代码全部抽取到一个方法当中）xml配置版实例代码接口：public interface Hello { void sayHello(); } 实现类public class HelloImpl implements Hello { @Override public void sayHello() { System.out.println("hello"); } }增强（相当于抽取出来的部分代码）public class BuildHello { void beforeBuilder(){ System.out.println("start..."); } void afterBuilder(){ System.out.println("ending..."); } } xml中bean实例的配置 <bean id="hello" class="com.soft1851.spring.aop.hello.HelloImpl"/> <bean id="beforeAdvice" class="com.soft1851.spring.aop.hello.BuildHello"/> <aop:config > <aop:aspect id="before" ref="beforeAdvice"> <aop:pointcut id="beforeCut" expression="execution(* com.soft1851.spring.aop.hello..*.*(..))"/> <aop:before method="beforeBuilder" pointcut-ref="beforeCut"/> </aop:aspect> <aop:aspect id="after" ref="beforeAdvice"> <aop:pointcut id="afterCut" expression="execution(* com.soft1851.spring.aop.hello..*.*(..))"/> <aop:after-returning method="afterBuilder" pointcut-ref="afterCut"/> </aop:aspect> </aop:config>注意要点：aop-config是配置整体的切面编程，而aop-aspect是设置切点和增强的容器，aop-pointcun用于设置切点，aop:before用于设置前置增强，aop:after-returning用于设置后置增强注解版实例代码dao底层代码@Repository public class ForumDaoImpl implements ForumDao { private JdbcTemplate jdbcTemplate; public ForumDaoImpl(JdbcTemplate jdbcTemplate) { this.jdbcTemplate = jdbcTemplate; } @Override public List<Forum> selectAll() { String sql = "SELECT * FROM t_forum "; System.out.println(jdbcTemplate); return jdbcTemplate.query(sql, new BeanPropertyRowMapper<>(Forum.class)); } @Override public int insert(Forum forum) { System.out.println("开始插入数据"); String sql = "INSERT INTO t_forum VALUES (Null,?)"; Object[] args = {forum.getForumName()}; return jdbcTemplate.update(sql, args); } @Override public int[] batchInsert(List<Forum> forums) { String sql = "INSERT INTO t_forum VALUES (NULL,?)"; return jdbcTemplate.batchUpdate(sql, new BatchPreparedStatementSetter() { @Override public void setValues(PreparedStatement preparedStatement, int i) throws SQLException { preparedStatement.setString(1,forums.get(i).getForumName()); } @Override public int getBatchSize() { return forums.size(); } }); } @Override public int delete(int forumId) { System.out.println("开始删除数据"); String sql = "DELETE FROM t_forum WHERE forum_id = ?"; Object args = forumId; return jdbcTemplate.update(sql,args); } /** * 修改forum中的字段值 * @param forum * @return */ @Override public int update(Forum forum) { String sql = "UPDATE t_forum SET forum_name = ? WHERE forum_id = ?"; Object[] args = {forum.getForumName(), forum.getForumId()}; return jdbcTemplate.update(sql, args); } @Override public Forum get(int forumId) { String sql = " SELECT * FROM t_forum WHERE forum_id = ? "; Object[] args = {forumId}; return jdbcTemplate.queryForObject(sql, args,new BeanPropertyRowMapper<>(Forum.class)); } } 增强类@Aspect public class MyAdvice { /** * 配置切点 */ @Pointcut("execution(* com.soft1851.spring.web.dao..*.insert*(..))") public void pointCut() { } /** * 配置前置增强 */ @Before("MyAdvice.pointCut()") public void beforeMethod() { System.out.println("等待数据插入"); } /** * 配置后置增强 */ @AfterReturning("execution(* com.soft1851.spring.web.dao..*.*(..))") public void afterMethod(){ System.out.println("关闭数据库"); } } xml实例配置 <bean name="forumDao" class="com.soft1851.spring.web.dao.impl.ForumDaoImpl"/> <bean name="myAdvice" class="com.soft1851.spring.web.interceptor.MyAdvice"/> <aop:aspectj-autoproxy/>注意要点：增强类头部要写注解@Aspectxml文件中照样还需要配置，只是配置代码减少@afterReturning是后置增强（也就是中央代码执行结束后执行）aop支持的切点execution——匹配运行方法的连接点within——使匹配连接点限定在特定类型this——限定匹配的连接点是给定类型的实例target——限定匹配连接点的目标是给定类型的实例args——限定连接点的参数是给定类型的实例@target——限定匹配的连接点的运行的对象有该类型的注解@args——运行时传递的参数拥有给定类型的注解@within——限定匹配给定指定注解类型的连接点@annotation——限定连接点拥有指定的注解bean——使得连接点匹配特质的bean或bean集合exexction表达式匹配所有的公有方法execution(public (..))匹配所有以set开头的方法execution( set(..))匹配所有AccountService接口定义的方法execution( com.xyz.service.AccountService.(..))匹配所有service包内的方法execution(* com.xyz.service..(..))匹配所有service包以及子包内的方法execution(* com.xyz.service...(..))所有在service包内的连接点within(com.xyz.service.*)所有service包以及子包内的连接点within(com.xyz.service..*)所有实现AccountService接口的连接点this(com.xyz.service.AccountService)所有的目标对象实现AccountService接口的连接点target(com.xyz.service.AccountService)有一个参数并且运行时传入参数为Serializable的连接点args(java.io.Serializable)目标对象有@Transactional注解@target(org.springframework.transaction.annotation.Transactional)定义的目标对象类型含有@Transactional注解@within(org.springframework.transaction.annotation.Transactional)含有@Transactional注解的可运行方法@annotation(org.springframework.transaction.annotation.Transactional)含有一个参数并且在运行时传入的参数含有@Classified注解@args(com.xyz.security.Classified)bean名字定义为tradeService的连接点bean(tradeService)bean名字满足Service命名的所有连接点bean(Service)总结本文我们主要学习了如何使用XML配置方式来完成Spring AOP的使用。文章中我们学习了很多配置标签，完成了切面定义、切点定义以及多种增强方法的定义。至此我们已经掌握了Spring AOP的基本知识点，可在具体的项目开发中使用。不过学了那么多的知识点，我们还是需要一定的实践来进行消化吸收。