第7章 系统管理-权限设置
学习目标
了解认证和授权的概念
掌握Spring Security入门案例开发过程
掌握Spring Security实现认证的过程
掌握Spring Security实现授权的过程
1. 权限控制、SpringSecurity入门及进阶
【目标】
了解认证和授权的概念
【路径】
1:认证和授权的概念
- 认证:登录(用户名和密码)
- 授权:访问系统功能的权限
2:权限模块的数据模型
- 用户表
- 角色表
- 权限表
- 菜单表
用户,角色是多对多
权限,角色是多对多
菜单,角色是多对多
【讲解】
1.1. 认证和授权概念【理解】
前面我们已经完成了传智健康后台管理系统(health_web)的部分功能,例如检查项管理、检查组管理、套餐管理、预约设置等。接下来我们需要思考2个问题:
问题1:在生产环境下我们如果不登录后台系统就可以完成这些功能操作吗?
答案显然是否定的,要操作这些功能必须首先登录到系统才可以。(用户登录系统—>认证)
问题2:是不是所有用户,只要登录成功就都可以操作所有功能呢?
答案是否定的,并不是所有的用户都可以操作这些功能。不同的用户可能拥有不同的权限,这就需要进行授权了。(用户登录之后,对每个用户进行授权,通过授权去访问系统中不同的功能—>授权)
认证:系统提供的用于识别用户身份的功能,通常提供用户名和密码进行登录其实就是在进行认证,认证的目的是让系统知道你是谁。
说白了:登陆系统了,就可以访问,没登陆就不能访问。验证用户名与密码是否存在数据库中(t_user)
授权:用户认证成功后,需要为用户授权,其实就是指定当前用户可以操作哪些功能。
用户登陆后,查询获取用户所拥有的权限绑定到用户, 当进行权限验证时就可以取用与登陆用户绑定的权限与访问的资源标定的权限进行匹配
商店里的 矿泉水 3块钱 资源权限标定。标定好了需要访问这个资源时的权限
登陆用户 有多少钱?老婆给予的,老婆授权。
授权是为了做权限验证的。登陆用户身上所拥有的权限集合
/checkitem/findPage.do 查询检查项 只有健康管理师才可以访问
员工A 来登陆 (查看套餐的权限) 来访 查询检查项(/checkitem/findPage.do 健康管理师) 报没有权限
认证与授权的目的,更好的保护资源(使用后台管理功能)
本章节就是要对后台系统进行权限控制,其本质就是对用户进行认证和授权。
1.2. 权限模块数据模型
前面已经分析了认证和授权的概念,要实现最终的权限控制,需要有一套表结构支撑:
用户表t_user、权限表t_permission、角色表t_role、菜单表t_menu、用户角色关系表t_user_role、角色权限关系表t_role_permission、角色菜单关系表t_role_menu。
表之间关系如下图:
通过上图可以看到,权限模块共涉及到7张表。在这7张表中,角色表起到了至关重要的作用,其处于核心位置,我们把基于角色的权限控制叫做RBAC,因为用户、权限、菜单都和角色是多对多关系。
接下来我们可以分析一下在认证和授权过程中分别会使用到哪些表:
认证过程:只需要用户表就可以了,在用户登录时可以查询用户表t_user进行校验,判断用户输入的用户名和密码是否正确。
授权过程:用户必须完成认证之后才可以进行授权,首先可以根据用户查询其角色,再根据角色查询对应的菜单,这样就确定了用户能够看到哪些菜单。然后再根据用户的角色查询对应的权限,这样就确定了用户拥有哪些权限。所以授权过程会用到上面7张表。
【小结】
认证和授权
- 认证: 提供账号和密码进行登录认证, 系统知道你的身份
- 授权: 根据不同的身份, 赋予不同的权限,不同的权限,可访问系统不同的功能( 系统的功能也要标定访问权限)
RBAC权限模块数据模型(基于角色的权限控制)
- 用户表
- 角色表
- 权限表
- 菜单表
1.3. Spring Security简介
【目标】
知道什么是Spring Security
【路径】
- Spring Security简介
- Spring Security使用需要的坐标
【讲解】
Spring Security是 Spring提供的安全认证服务的框架。 使用Spring Security可以帮助我们来简化认证和授权的过程。官网:https://spring.io/projects/spring-security
对应的maven坐标:
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-web</artifactId>
<version>5.0.5.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-config</artifactId>
<version>5.0.5.RELEASE</version>
</dependency>
常用的权限框架除了Spring Security,还有Apache的shiro框架。
【小结】
- SpringSecurity是Spring家族的一个安全框架, 简化我们开发里面的认证和授权过程, (登陆, 访问url时的权限控制 secuirty帮我们做了)
- SpringSecurity内部封装了Filter(只需要在web.xml容器中配置一个过滤器—代理过滤器,真实的过滤器(12个)在spring的容器中配置)
常见的安全框架
- Spring的 SpringSecurity 配置复杂 有个好老爸(spring) 无缝整合 链式模式
- Apache的Shiro
http://shiro.apache.org/
比较简单 apache 写整合 外观者模式
1.4. Spring Security入门案例-【重点】
【目标】
【需求】
使用Spring Security进行控制: 网站(一些页面)需要登录才能访问(认证)
【路径】
- 创建Maven health_parent的子工程 springsecurity_demo,导入坐标(依赖health_interface)
- 配置web.xml(前端控制器加载spring, SpringSecurity代理过滤器)
- 创建spring-security.xml(核心配置文件, spring要加载这个配置)
【讲解】
1.4.1. 工程搭建
创建maven工程,打包方式为war,为了方便起见我们可以让入门案例工程依赖health_interface,这样相关的依赖都继承过来了。
pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.itheima</groupId>
<artifactId>springsecurity_demo</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>war</packaging>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>com.itheima</groupId>
<artifactId>health_interface</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
</dependencies>
</project>
内置提供index.html页面,内容为“登录成功”!!
1.4.2. 配置web.xml
【路径】
1:DelegatingFilterProxy用于整合第三方框架(代理过滤器,非真正的过滤器,真正的过滤器需要在spring的配置文件)
2:springmvc的核心控制器
在web.xml中主要配置SpringMVC的DispatcherServlet和用于整合第三方框架的DelegatingFilterProxy(代理过滤器,真正的过滤器在spring的配置文件),用于整合Spring Security。
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee
http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
version="3.0">
<filter>
<!-- filter-name:固定为springSecurityFilterChain,不能修改。容器启动时,
会从spring容器中获取这个filtername的bean对象(security启动时创建的bean name)
可以修改:必须在spring容器中配置一个bean对象为你修改后的名称
-->
<filter-name>springSecurityFilterChain</filter-name>
<!-- 代理过滤:
自己不干活,拦截的请求转给security的过滤器(springSecurityFilterChain)去处理
-->
<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
</filter>
<filter-mapping>
<filter-name>springSecurityFilterChain</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<!-- 使用DispatcherServlet或者使用contextLoaderListener 都一样,只要启动容器即可 -->
<servlet>
<servlet-name>dispatchServlet</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:spring-security.xml</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>dispatchServlet</servlet-name>
<url-pattern>*.do</url-pattern>
</servlet-mapping>
</web-app>
1.4.3. 配置spring-security.xml
【路径 】
1:定义哪些链接可以放行
2:定义哪些链接不可以放行,即需要有角色、权限才可以放行
3:认证管理,定义登录账号名和密码,并授予访问的角色、权限
在spring-security.xml中主要配置Spring Security的拦截规则和认证管理器。
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:security="http://www.springframework.org/schema/security"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security.xsd">
<!--
【重要】【重要】【重要】
这里面的所有路径必须以/开头,否则启动报错
-->
<!-- auto-config: 自动配置,自动生成login页面,login处理,退出处理
use-expressions: 是否使用spel表达式 true: access的值可以填表达式(hasRole, hasAuthority, hasAny....)
false: ROEL_角色名(必须是ROLE_打,否则启动报错), 或 security写死的几个常量
-->
<!-- 拦截规则配置 -->
<security:http auto-config="true" use-expressions="false">
<!-- pattern="/**" 拦截所有的路径 access="ROLE_ADMIN"
要访问符合pattern的url时,登陆用户必须有ROLE_ADMIN的角色,如果没有则不能访问
security:intercept-url: 可以配置多个
-->
<security:intercept-url pattern="/**" access="ROLE_ADMIN"/>
</security:http>
<!-- 认证管理器 -->
<security:authentication-manager>
<!-- 认证信息提供者,认证信息的来源
提供登陆用户信息 用户名、密码、权限集合
user-service-ref 指向spring容器中一个bean对象, 实现这个UserDetailsService的对象, 提供用户的名称、密码、权限集合
一旦配置了user-service-ref,就不要配置security:user-service
-->
<security:authentication-provider>
<!-- 登陆用户信息由我们自己来提供 -->
<security:user-service>
<!-- security:user 硬编码一个用户对象在内存中,就不需要去查询数据库了
将来应该使用从数据库查询
name: 登陆的用户名 password:登陆的密码
authorities: 指定的权限(既可以是角色名也可以权限名) authorities与上面access一致才能访问
{noop}使用的是明文,no operation 不要对密码做处理
-->
<security:user name="admin" password="{noop}admin" authorities="ROLE_ADMIN"/>
</security:user-service>
</security:authentication-provider>
</security:authentication-manager>
</beans>
{noop}:表示当前使用的密码为明文。表示当前密码不需要加密
自动跳转到登录页面(springSecurity自动提供的)
输入错误用户名和密码
输入正确用户名和密码(admin/admin)
此时说明没有登录成功的页面。
如果新建index.html,可以正常访问index.html
【小结】
使用步骤
- 创建Maven工程, 添加坐标
- 配置web.xml(前端控制器,springSecurity权限相关的过滤器 DelegatingFilterProxy, 过滤器的名称不能修改,一定是springSecurityFilterChain)
- 创建spring-security.xml(security:http 自动配置,使用表达式的方式完成授权,只要具有ROLE_ADMIN的角色权限才能访问系统中的所有功能; 授权管理器,指定用户名admin,密码{noop}admin,具有ROLE_ADMIN的角色权限)
注意实现
1.在web.xml里面配置的权限相关的过滤器名字不能改(springSecurityFilterChain)
<filter>
<filter-name>springSecurityFilterChain</filter-name>
<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
</filter>
<filter-mapping>
<filter-name>springSecurityFilterChain</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
2.入门案例里面没有指定密码加密方式的. 配置密码的时候的加{noop}
<security:user-service>
<security:user name="admin" password="{noop}admin" authorities="ROLE_ADMIN"/>
</security:user-service>
1.5. Spring Security进阶
【目标】
前面我们已经完成了Spring Security的入门案例,通过入门案例我们可以看到,Spring Security将我们项目中的所有资源都保护了起来,要访问这些资源必须要完成认证而且需要具有ROLE_ADMIN角色。
但是入门案例中的使用方法离我们真实生产环境还差很远,还存在如下一些问题:
1、项目中我们将所有的资源(所有请求URL pattern=/**)都保护起来,实际环境下往往有一些资源不需要认证也可以访问,也就是可以匿名访问(静态资源)。
2、登录页面是由框架生成的,而我们的项目往往会使用自己的登录页面。
3、直接将用户名和密码配置在了配置文件中,而真实生产环境下的用户名和密码往往保存在数据库中。
4、在配置文件中配置的密码使用明文,这非常不安全,而真实生产环境下密码需要进行加密。
本章节需要对这些问题进行改进。
【路径】
1:配置可匿名访问的资源(不需要登录,权限 角色 就可以访问的资源)
2:使用指定的登录页面(login.html)
3:从数据库查询用户信息
4:对密码进行加密
5:配置多种校验规则(对访问的页面做权限控制)
6:注解方式权限控制(对访问的Controller类做权限控制)
7:退出登录
【讲解】
1.5.1. 配置可匿名访问的资源
【路径】
1:在项目中创建js、css目录并在两个目录下提供任意一些测试文件
2:在spring-security.xml文件中配置,指定哪些资源可以匿名访问
第一步:在项目中创建js、css目录并在两个目录下提供任意一些测试文件
访问http://localhost:85/js/vue.js
第二步:在spring-security.xml文件中配置,指定哪些资源可以匿名访问
<!--
http:用于定义相关权限控制
指定哪些资源不需要进行权限校验,可以使用通配符
-->
<security:http security="none" pattern="/js/**" />
<security:http security="none" pattern="/css/**" />
通过上面的配置可以发现,js和css目录下的文件可以在没有认证的情况下任意访问。
1.5.2. 使用指定的登录页面
【路径】
1:提供login.html作为项目的登录页面
2:修改spring-security.xml文件,指定login.html页面可以匿名访问
3:修改spring-security.xml文件,加入表单登录信息的配置
4:修改spring-security.xml文件,关闭csrfFilter过滤器
【讲解】
第一步:提供login.html作为项目的登录页面
1:用户名是abc
2:密码是bbb
3:登录的url是/login
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<form name='f' action='/login' method='POST'>
<table>
<tr><td>用户名:</td><td><input type='text' name='abc' value=''></td></tr>
<tr><td>密码:</td><td><input type='password' name='bbb'/></td></tr>
<tr><td colspan='2'><input name="submit" type="submit" value="登陆"/></td></tr>
</table>
</form>
</body>
</html>
第二步:修改spring-security.xml文件,指定login.html页面可以匿名访问,否则无法访问。
<security:http security="none" pattern="/login.html" />
第三步:修改spring-security.xml文件,加入表单登录信息的配置
<!-- 登陆配置 form-login
login-page: 登陆页面
username-parameter: 前端传过来的用户名的参数名
password-parameter: 前端传过来的密码的参数名
login-processing-url: 处理登陆请求的url
default-target-url: 登陆成功后默认跳转的页面, success.html -> login.html->success.html
always-use-default-target: true不管是从哪个页面转到login.html,登陆后都跑到default-target-url
success.html -> login.html-> index.html
authentication-failure-url: 登陆失败后跳转的页面
-->
<security:form-login
login-page="/login.html"
username-parameter="abc"
password-parameter="bbb"
login-processing-url="/login"
default-target-url="/index.html"
always-use-default-target="true"
authentication-failure-url="/fail.html"
></security:form-login>
第四步:修改spring-security.xml文件,关闭CsrfFilter过滤器
1.5.2.1. 注意1:
如果用户名和密码输入不正确/正确。抛出异常:
分析原因:
Spring-security采用盗链机制,其中_csrf使用token标识和随机字符,每次访问页面都会随机生成,然后和服务器进行比较,成功可以访问,不成功不能访问(403:权限不足)。
解决方案:
<!--关闭盗链安全请求-->
<security:csrf disabled="true" />
1.5.2.2. 注意2:
1:创建test.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
我是test页面
</body>
</html>
2:先访问test.html页面,跳转到login.html
3:再使用admin、admin登录,会跳转到test.html,怎么办?
分析原因:
登录成功后,没有始终跳转到成功页面,而是跳转到了之前访问的页面。
解决方案:
always-use-default-target=”true”
<!--
登录页面配置
login-page:登录页面
login-processing-url:处理登录的地址
default-target-url:登录成功后默认跳转地址
authentication-failure-url:登录失败跳转地址
always-use-default-target="true":登录成功后,始终跳转到default-target-url指定的地址,即登录成功的默认地址
-->
<security:form-login login-page="/login.html"
username-parameter="username"
password-parameter="password"
login-processing-url="/login.do"
default-target-url="/index.html"
authentication-failure-url="/login.html"
always-use-default-target="true"
/>
1.5.3. 从数据库查询用户信息
【路径】
1:定义UserService类,实现UserDetailsService接口。
2:修改spring-security.xml配置(注册且注入UserService)
如果我们要从数据库动态查询用户信息,就必须按照spring security框架的要求提供一个实现UserDetailsService接口的实现类,并按照框架的要求进行配置即可。框架会自动调用实现类中的方法并自动进行密码校验。
第一步:定义UserService类,实现UserDetailsService接口。
实现类代码:
package com.itheima.security;
import com.google.gson.internal.$Gson$Preconditions;
import com.itheima.health.pojo.Permission;
import com.itheima.health.pojo.Role;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
/**
* Description: 将来要进入spring容器, 服务也可以注入进来
* User: Eric
*/
public class UserService implements UserDetailsService {
/**
* 通过用户名加载用户信息 User登陆用
* @param username
* @return
* @throws UsernameNotFoundException
*/
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//提供用户的名称、密码、权限集合
// 通过用户名来查询数据库, 查询角色及权限
com.itheima.health.pojo.User userInDB = findByUsername(username);
// String username,
// String password, 数据库中的密码, 密码校验(security帮我们做了)
// Collection<? extends GrantedAuthority> authorities 权限集合
List<GrantedAuthority> authorities = new ArrayList<GrantedAuthority>();
// 遍历用户身上的角色
Set<Role> roles = userInDB.getRoles();
if(null != roles){
for (Role role : roles) {
// 授予的权限
// 构造方法要的是一个角色名
GrantedAuthority ga = new SimpleGrantedAuthority(role.getName());
authorities.add(ga);
for (Permission permission : role.getPermissions()) {
// 权限
ga = new SimpleGrantedAuthority(permission.getName());
authorities.add(ga);
}
}
}
// 登陆用户的认证信息,名称、密码、权限集合
User user = new User(username,"{noop}" + userInDB.getPassword(),authorities);
return user;
}
/**
* 假设从数据库查询
* @param username
* @return
*/
private com.itheima.health.pojo.User findByUsername (String username){
if("admin".equals(username)) {
com.itheima.health.pojo.User user = new com.itheima.health.pojo.User();
user.setUsername("admin");
user.setPassword("admin");
Role role = new Role();
role.setName("ROLE_ADMIN");
Permission permission = new Permission();
permission.setName("ADD_CHECKITEM");
role.getPermissions().add(permission);
Set<Role> roleList = new HashSet<Role>();
roleList.add(role);
user.setRoles(roleList);
return user;
}
return null;
}
public static void main(String[] args) {
BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
// 加密密码
//System.out.println(encoder.encode("1234"));
// 验证密码
// 原密码
// 加密后的密码
System.out.println(encoder.matches("1234", "$2a$10$P7Qx8eKUPX5lngz9UEstUOaDRldEWrj9Rbyox/ShyeoxvPbEHTvni"));
System.out.println(encoder.matches("1234", "$2a$10$5q.0a0F0hRix8TBJxQ4DB.ekwGzPs3e47hoQVNR7cihi/Rob.G3T6"));
System.out.println(encoder.matches("1234", "$2a$10$voh.1PJRXQazoijK72sIoOslpmLYPyB.6LtT7aUrXqUO5G8Aw43we"));
System.out.println(encoder.matches("1234", "$2a$10$u/BcsUUqZNWUxdmDhbnoeeobJy6IBsL1Gn/S0dMxI2RbSgnMKJ.4a"));
}
}
第二步:spring-security.xml:
本章节我们提供了UserService实现类,并且按照框架的要求实现了UserDetailsService接口。在spring配置文件中注册UserService,指定其作为认证过程中根据用户名查询用户信息的处理类。当我们进行登录操作时,spring security框架会调用UserService的loadUserByUsername方法查询用户信息,并根据此方法中提供的密码和用户页面输入的密码进行比对来实现认证操作。
1.5.4. 对密码进行加密
前面我们使用的密码都是明文的,这是非常不安全的。一般情况下用户的密码需要进行加密后再保存到数据库中。
常见的密码加密方式有:
3DES、AES、DES:使用对称加密算法,可以通过解密来还原出原始密码 可逆加密
MD5、SHA1:使用单向HASH算法,无法通过计算还原出原始密码,但是可以建立彩虹表进行查表破解
MD5可进行加盐加密,保证安全
public class TestMD5 {
@Test
public void testMD5(){
// 密码同样是1234却变成了密码不相同
System.out.println(MD5Utils.md5("1234xiaowang")); //a8231077b3d5b40ffadee7f4c8f66cb7
System.out.println(MD5Utils.md5("1234xiaoli")); //7d5250d8620fcdb53b25f96a1c7be591
}
}
同样的密码值,盐值不同,加密的结果不同。
bcrypt:将salt随机并混入最终加密后的密码,验证时也无需单独提供之前的salt,从而无需单独处理salt问题
spring security中的BCryptPasswordEncoder方法采用SHA-256 +随机盐+密钥对密码进行加密。SHA系列是Hash算法,不是加密算法,使用加密算法意味着可以解密(这个与编码/解码一样),但是采用Hash处理,其过程是不可逆的。
(1)加密(encode):注册用户时,使用SHA-256+随机盐+密钥把用户输入的密码进行hash处理,得到密码的hash值,然后将其存入数据库中。
(2)密码匹配(matches):用户登录时,密码匹配阶段并没有进行密码解密(因为密码经过Hash处理,是不可逆的),而是使用相同的算法把用户输入的密码进行hash处理,得到密码的hash值,然后将其与从数据库中查询到的密码hash值进行比较。如果两者相同,说明用户输入的密码正确。
这正是为什么处理密码时要用hash算法,而不用加密算法。因为这样处理即使数据库泄漏,黑客也很难破解密码。
建立测试代码
package com.itheima.security.test;
import org.junit.Test;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
public class TestSpringSecurity {
// SpringSecurity加盐加密
@Test
public void testSpringSecurity(){
BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
// 加密密码
//System.out.println(encoder.encode("1234"));
// 验证密码
// 原密码
// 加密后的密码
System.out.println(encoder.matches("1234", "$2a$10$P7Qx8eKUPX5lngz9UEstUOaDRldEWrj9Rbyox/ShyeoxvPbEHTvni"));
System.out.println(encoder.matches("1234", "$2a$10$5q.0a0F0hRix8TBJxQ4DB.ekwGzPs3e47hoQVNR7cihi/Rob.G3T6"));
System.out.println(encoder.matches("1234", "$2a$10$voh.1PJRXQazoijK72sIoOslpmLYPyB.6LtT7aUrXqUO5G8Aw43we"));
System.out.println(encoder.matches("1234", "$2a$10$u/BcsUUqZNWUxdmDhbnoeeobJy6IBsL1Gn/S0dMxI2RbSgnMKJ.4a"));
}
}
加密后的格式一般为:
$2a$10$/bTVvqqlH9UiE0ZJZ7N2Me3RIgUCdgMheyTgV0B4cMCSokPa.6oCa
加密后字符串的长度为固定的60位。其中:
$是分割符,无意义;
2a是bcrypt加密版本号;
10是cost的值;
而后的前22位是salt值;
再然后的字符串就是密码的密文了。
实现步骤:
【路径】
1:在spring-security.xml文件中指定密码加密对象
2:修改UserService实现类
【讲解】
第一步:在spring-security.xml文件中指定密码加密对象
<!--
三:认证管理,定义登录账号名和密码,并授予访问的角色、权限
authentication-manager:认证管理器,用于处理认证操作
-->
<security:authentication-manager>
<!--
authentication-provider:认证提供者,执行具体的认证逻辑
-->
<security:authentication-provider user-service-ref="userService">
<!--指定密码加密策略-->
<security:password-encoder ref="encoder"></security:password-encoder>
</security:authentication-provider>
</security:authentication-manager>
<!--配置密码加密对象-->
<bean id="encoder"
class="org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder" />
第二步:修改UserService实现类
package com.itheima.security;
import com.google.gson.internal.$Gson$Preconditions;
import com.itheima.health.pojo.Permission;
import com.itheima.health.pojo.Role;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
/**
* Description: 将来要进入spring容器, 服务也可以注入进来
* User: Eric
*/
public class UserService implements UserDetailsService {
/**
* 通过用户名加载用户信息 User登陆用
* @param username
* @return
* @throws UsernameNotFoundException
*/
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//提供用户的名称、密码、权限集合
// 通过用户名来查询数据库, 查询角色及权限
com.itheima.health.pojo.User userInDB = findByUsername(username);
// String username,
// String password, 数据库中的密码, 密码校验(security帮我们做了)
// Collection<? extends GrantedAuthority> authorities 权限集合
List<GrantedAuthority> authorities = new ArrayList<GrantedAuthority>();
// 遍历用户身上的角色
/*Set<Role> roles = userInDB.getRoles();
if(null != roles){
for (Role role : roles) {
// 授予的权限
// 构造方法要的是一个角色名
GrantedAuthority ga = new SimpleGrantedAuthority(role.getName());
authorities.add(ga);
for (Permission permission : role.getPermissions()) {
// 权限
ga = new SimpleGrantedAuthority(permission.getName());
authorities.add(ga);
}
}
}*/
//测试其它方式的认证
GrantedAuthority ga = new SimpleGrantedAuthority("ROLE_ADMIN");
authorities.add(ga);
ga = new SimpleGrantedAuthority("add");
authorities.add(ga);
// 登陆用户的认证信息,名称、密码、权限集合
//User user = new User(username,"{noop}" + userInDB.getPassword(),authorities);
// 使用加密的密码后,去除{noop}
User user = new User(username,userInDB.getPassword(),authorities);
return user;
}
/**
* 假设从数据库查询
* @param username
* @return
*/
private com.itheima.health.pojo.User findByUsername (String username){
if("admin".equals(username)) {
com.itheima.health.pojo.User user = new com.itheima.health.pojo.User();
user.setUsername("admin");
//user.setPassword("admin");
// 使用密码加密器encoder, 加密后的密码
user.setPassword("$2a$10$P7Qx8eKUPX5lngz9UEstUOaDRldEWrj9Rbyox/ShyeoxvPbEHTvni");
Role role = new Role();
role.setName("ROLE_ADMIN");
Permission permission = new Permission();
permission.setName("ADD_CHECKITEM");
role.getPermissions().add(permission);
Set<Role> roleList = new HashSet<Role>();
roleList.add(role);
user.setRoles(roleList);
return user;
}
return null;
}
public static void main(String[] args) {
BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
// 加密密码
//System.out.println(encoder.encode("1234"));
// 验证密码
// 原密码
// 加密后的密码
System.out.println(encoder.matches("1234", "$2a$10$P7Qx8eKUPX5lngz9UEstUOaDRldEWrj9Rbyox/ShyeoxvPbEHTvni"));
System.out.println(encoder.matches("1234", "$2a$10$5q.0a0F0hRix8TBJxQ4DB.ekwGzPs3e47hoQVNR7cihi/Rob.G3T6"));
System.out.println(encoder.matches("1234", "$2a$10$voh.1PJRXQazoijK72sIoOslpmLYPyB.6LtT7aUrXqUO5G8Aw43we"));
System.out.println(encoder.matches("1234", "$2a$10$u/BcsUUqZNWUxdmDhbnoeeobJy6IBsL1Gn/S0dMxI2RbSgnMKJ.4a"));
}
}
使用密码加密器小结:
- security.xml添加bean 加密器
<!-- 加密器-->
<bean id="encoder" class="org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder"/>
security.xml使用加密器
<security:authentication-provider user-service-ref="userService">
<!-- provider调用 service 来获取认证用户信息 -->
<!-- <security:user-service>
写死内存中一个用户信息
authorities: 这个用户所拥有的权限/角色
明文密码: noop no operation
密码加密器:默认使用BcyptPasswordEncoder
<security:user name="admin" authorities="ROLE_ADMIN" password="{noop}admin"/>
</security:user-service> -->
<security:password-encoder ref="encoder"/>
</security:authentication-provider>
- 修改UserService中admin用户的密码为密文, 去{noop}
1.5.5. 配置多种验证规则(对页面)
为了测试方便,首先在项目中创建a.html、b.html、c.html、d.html几个页面
修改spring-security.xml文件:
前提:
<!--只要认证通过就可以访问-->
<security:intercept-url pattern="/index.html" access="isAuthenticated()" />
<security:intercept-url pattern="/a.html" access="isAuthenticated()" />
<!--拥有add权限就可以访问b.html页面-->
<security:intercept-url pattern="/b.html" access="hasAuthority('add')" />
<!--拥有ROLE_ADMIN角色就可以访问c.html页面,
注意:此处虽然写的是ADMIN角色,框架会自动加上前缀ROLE_-->
<security:intercept-url pattern="/c.html" access="hasRole('ADMIN')" />
<!--拥有ROLE_ADMIN角色就可以访问d.html页面-->
<security:intercept-url pattern="/d.html" access="hasRole('ABC')" />
测试:
登录后可以访问a.html,b.html,c.html,不能访问d.html(抛出403的异常)
1.5.6. 注解方式权限控制(对类)
Spring Security除了可以在配置文件中配置权限校验规则,还可以使用注解方式控制类中方法的调用。例如Controller中的某个方法要求必须具有某个权限才可以访问,此时就可以使用Spring Security框架提供的注解方式进行控制。
【路径】
1:在spring-security.xml文件中配置组件扫描和mvc的注解驱动,用于扫描Controller
2:在spring-security.xml文件中开启权限注解支持
3:创建Controller类并在Controller的方法上加入注解(@PreAuthorize)进行权限控制
实现步骤:
第一步:在spring-security.xml文件中配置组件扫描,用于扫描Controller
<context:component-scan base-package="com.itheima"/>
<mvc:annotation-driven></mvc:annotation-driven>
第二步:在spring-security.xml文件中开启权限注解支持
<!--开启注解方式权限控制-->
<security:global-method-security pre-post-annotations="enabled" />
第三步:创建Controller类并在Controller的方法上加入注解(@PreAuthorize)进行权限控制
package com.itheima.controller;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
@Controller
@RequestMapping("/hello")
public class HelloController {
@RequestMapping("/add")
@PreAuthorize("hasAuthority('add')")//表示用户必须拥有add权限才能调用当前方法
public String add(){
System.out.println("add...");
return null;
}
@RequestMapping("/update")
@PreAuthorize("hasRole('ROLE_ADMIN')")//表示用户必须拥有ROLE_ADMIN角色才能调用当前方法
public String update(){
System.out.println("update...");
return null;
}
@RequestMapping("/delete")
@PreAuthorize("hasRole('ABC')")//表示用户必须拥有ABC角色才能调用当前方法
public String delete(){
System.out.println("delete...");
return null;
}
}
测试delete方法不能访问
小结:
- 开启注解支持
<security:global-method-security pre-post-annotations="enabled"/>
使用mvc:annotation-driven
创建controller,且加入扫包context:component-scan
controller方法@PreAuthorize(hasAuthority/hasRole)
注解掉
<!--<security:intercept-url pattern="/**" access="hasRole('ROLE_ADMIN')"/>-->
URL拦截权限配置,粗颗粒
@PreAuthrize 细颗粒(具体的方法) @PreAuthrize用controller类上
如果类有PreAuthrize, 方法上也有PreAuthrize, 两个条件都满足后才可以访问
推荐:使用角色来做权限控制
1.5.7. 退出登录
用户完成登录后Spring Security框架会记录当前用户认证状态为已认证状态,即表示用户登录成功了。那用户如何退出登录呢?我们可以在spring-security.xml文件中进行如下配置:
【路径】
1:index.html定义退出登录链接
2:在spring-security.xml定义
【讲解】
第一步:index.html定义退出登录链接
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
登录成功!<br>
<a href="/logout">退出登录</a>
</body>
</html>
第二步:在spring-security.xml定义:
<!--
logout:退出登录
logout-url:退出登录操作对应的请求路径
logout-success-url:退出登录后的跳转页面
-->
<security:logout logout-url="/logout"
logout-success-url="/login.html" invalidate-session="true"/>
通过上面的配置可以发现,如果用户要退出登录,只需要请求/logout.do这个URL地址就可以,同时会将当前session失效,最后页面会跳转到login.html页面。
【小结】
1:配置可匿名访问的资源(不需要登录,权限 角色 就可以访问)
<security:http security="none" pattern="/js/**"></security:http>
<security:http security="none" pattern="/css/**"></security:http>
<security:http security="none" pattern="/login.html"></security:http>
2:使用指定的登录页面(login.html)
<security:form-login login-page="/login.html"
username-parameter="username"
password-parameter="password"
login-processing-url="/login"
default-target-url="/index.html"
authentication-failure-url="/login.html"
always-use-default-target="true"/>
<!-- 关闭跨域访问限制 -->
<security:csrf disabled="true"/>
3:从数据库查询用户信息
添加类实现UserDetailService接口,实现loadByUsername方法,且要返回UserDetails对象(用户名,数据库中的密码,用户所拥有的权限集合)
<security:authentication-manager>
<security:authentication-provider user-service-ref="userService">
<security:password-encoder ref="encoder"></security:password-encoder>
</security:authentication-provider>
</security:authentication-manager>
4:对密码进行加密
<bean id="encoder" class="org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder"></bean>
userDetail中密码不再用{noop},把{noop}删除
5:配置多种校验规则(对访问的页面做权限控制, 多个时,从上往按顺序,只要有一个满足,就会处理)
use-expressions="true"
<security:intercept-url pattern="/index.html" access="isAuthenticated()"></security:intercept-url>
<security:intercept-url pattern="/a.html" access="isAuthenticated()"></security:intercept-url>
<security:intercept-url pattern="/b.html" access="hasAuthority('add')"></security:intercept-url>
<security:intercept-url pattern="/c.html" access="hasRole('ROLE_ADMIN')"></security:intercept-url>
<security:intercept-url pattern="/d.html" access="hasRole('ABC')"></security:intercept-url>
6:注解方式权限控制(对访问的Controller类做权限控制)
<security:global-method-security pre-post-annotations="enabled"></security:global-method-security>
同时使用注解:
在Controller类中的方法上添加:@PreAuthorize(value = “hasRole(‘ROLE_ADMIN’)”)
7:退出登录
<security:logout logout-url="/logout" logout-success-url="/login.html" invalidate-session="true"></security:logout>
2. 项目中使用Spring Security【重点】
前面我们已经学习了Spring Security框架的使用方法,本章节我们就需要将Spring Security框架应用到后台系统中进行权限控制,其本质就是认证和授权。
要进行认证和授权需要前面课程中提到的权限模型涉及的7张表支撑,因为用户信息、权限信息、菜单信息、角色信息、关联信息等都保存在这7张表中,也就是这些表中的数据是我们进行认证和授权的依据。所以在真正进行认证和授权之前需要对这些数据进行管理,即我们需要开发如下一些功能:
1、用户数据管理(增删改查、用户关联角色)
2、角色数据管理(增删改查、角色关联权限、角色关联菜单)
3、权限数据管理(增删改查)
4、菜单数据管理(增删改查)
鉴于时间关系,我们不再实现这些数据管理的代码开发。我们可以直接将数据导入到数据库中即可。
导入用户、角色、权限、菜单的初始数据
【目标】
在传智健康的项目中使用SpringSecurity完成认证和授权
【路径】
1:导入SpringSecurity环境
(1)pom.xml中添加依赖
(2)health_web web.xml添加代理过滤器
2:实现认证和授权
(1)导入login.html,放入health_web工程的webapp目录下
(2)认证:SpringSecurityUserService.java
(3)创建UserService类、UserDao接口类、UserDao映射文件
(4)springmvc.xml(dubbo注解扫描范围扩大)
(5)spring-security.xml
- 静态资源
- 拦截规则,所有都必须登陆后才可访问
- 登陆页面配置
- 关闭csrf
- frame访问策略
- 退出登陆
- 开启注解权限控制
认证管理器
- 认证信息提供者
- 加密器
(6)springmvc.xml(导入spring-security.xml)
(7)CheckItemController类(@PreAuthorize(“hasAuthority(‘CHECKITEM_ADD’)”):完成权限)
(8)捕获异常
3:显示用户名
4:用户退出
【讲解】
2.1. 导入Spring Security环境
【路径】
1:pom.xml导入坐标
2:web.xml添加代理过滤器
2.1.1. 第一步:pom.xml导入坐标
在health_parent父工程的pom.xml中导入Spring Security的maven坐标(已经引入)
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-web</artifactId>
<version>${spring.security.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-config</artifactId>
<version>${spring.security.version}</version>
</dependency>
2.1.2. 第二步:web.xml添加代理过滤器
在health_web工程的web.xml文件中配置用于整合Spring Security框架的过滤器DelegatingFilterProxy
<filter>
<!--
DelegatingFilterProxy用于整合第三方框架(代理过滤器,非真正的过滤器,真正的过滤器需要在spring的配置文件)
整合Spring Security时过滤器的名称必须为springSecurityFilterChain,
否则会抛出NoSuchBeanDefinitionException异常
-->
<filter-name>springSecurityFilterChain</filter-name>
<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
</filter>
<filter-mapping>
<filter-name>springSecurityFilterChain</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
2.2. 实现认证和授权
2.2.1. 第一步:导入login.html页面
此时login.html是可放行的页面,而pages下的页面必须认证之后才能访问的页面
2.2.2. 第二步:SpringSecurityUserService.java
在health_web工程中按照Spring Security框架要求提供SpringSecurityUserService,并且实现UserDetailsService接口
package com.itheima.health.security;
import com.alibaba.dubbo.config.annotation.Reference;
import com.itheima.health.pojo.Permission;
import com.itheima.health.pojo.Role;
import com.itheima.health.pojo.User;
import com.itheima.health.service.UserService;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
/**
* Description: 登陆用户认证与授权
* 记得要把这个类注册到spring容器
* User: Eric
*/
@Component
public class SpringSecurityUserService implements UserDetailsService {
@Reference
private UserService userService;
/**
* 提供登陆用户信息 username password 权限集合 authorities
* @param username
* @return
* @throws UsernameNotFoundException
*/
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//根据登陆用户名称查询用户权限信息
//t_user -> t_user_role -> t_role -> t_role_permission -> t_permission
//找出用户所拥有的角色,及角色下所拥有的权限集合
//User.roles(角色集合).permissions(权限集合)
User user = userService.findByUsername(username);
if(null != user){
// 用户名
// 密码
String password = user.getPassword();
// 权限集合
List<GrantedAuthority> authorities = new ArrayList<GrantedAuthority>();
// 授权
// 用户所拥有的角色
SimpleGrantedAuthority sai = null;
Set<Role> roles = user.getRoles();
if(null != roles){
for (Role role : roles) {
// 角色用关键字, 授予角色
sai = new SimpleGrantedAuthority(role.getKeyword());
authorities.add(sai);
// 权限, 角色下的所有权限
Set<Permission> permissions = role.getPermissions();
if(null != permissions){
for (Permission permission : permissions) {
// 授予权限
sai = new SimpleGrantedAuthority(permission.getKeyword());
authorities.add(sai);
}
}
}
}
return new org.springframework.security.core.userdetails.User(username,password,authorities);
}
// 返回null, 限制访问
return null;
}
}
使用debug跟踪调试,查看user。
2.2.3. 第三步:Service、Dao接口、Mapper映射文件
创建UserService服务接口、服务实现类、Dao接口、Mapper映射文件等
【路径】
1:UserService.java接口
2:UserServiceImpl.java类
3:UserDao.java(使用用户名称查询用户)
4:RoleDao.java(使用用户id查询角色集合)
5:PermissionDao.java(使用角色id查询权限集合)
6:UserDao.xml(使用用户名称查询用户)
7:RoleDao.xml(使用用户id查询角色集合)
8:PermissionDao.xml(使用角色id查询权限集合)
【讲解】
1:服务接口
package com.itheima.health.service;
import com.itheima.health.pojo.User;
/**
* Description: 用户服务(企业员工)
* User: Eric
*/
public interface UserService {
/**
* 根据登陆用户名称查询用户权限信息
* @param username
* @return
*/
User findByUsername(String username);
}
2:服务实现类
package com.itheima.health.service.impl;
import com.alibaba.dubbo.config.annotation.Service;
import com.itheima.health.dao.UserDao;
import com.itheima.health.pojo.User;
import com.itheima.health.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
/**
* Description: No Description
* User: Eric
*/
@Service(interfaceClass = UserService.class)
public class UserServiceImpl implements UserService {
@Autowired
private UserDao userDao;
/**
* 根据登陆用户名称查询用户权限信息
* @param username
* @return
*/
@Override
public User findByUsername(String username) {
return userDao.findByUsername(username);
}
}
3:Dao接口
(1)UserDao
package com.itheima.health.dao;
import com.itheima.health.pojo.User;
/**
* Description: No Description
* User: Eric
*/
public interface UserDao {
/**
* 根据登陆用户名称查询用户权限信息
* @param username
* @return
*/
User findByUsername(String username);
}
4:Mapper映射文件
(1)UserDao.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.itheima.health.dao.UserDao">
<select id="findByUsername" parameterType="String" resultMap="userRolePermissionMap">
select u.id,u.username,u.password,
ur.role_id, r.keyword role_keyword, r.name role_name,
rp.permission_id, p.keyword permission_keyword, p.name permission_name
From
t_user u, t_user_role ur, t_role r,
t_role_permission rp, t_permission p
where u.id=ur.user_id and ur.role_id=r.id
and r.id=rp.role_id and rp.permission_id=p.id
and u.username=#{username}
</select>
<resultMap id="userRolePermissionMap" type="User">
<id property="id" column="id"></id>
<result property="username" column="username"/>
<result property="password" column="password"/>
<collection property="roles" ofType="Role">
<id property="id" column="role_id"/>
<result property="keyword" column="role_keyword"/>
<result property="name" column="role_name"/>
<collection property="permissions" ofType="Permission">
<id property="id" column="permission_id"/>
<result property="keyword" column="permission_keyword"/>
<result property="name" column="permission_name"/>
</collection>
</collection>
</resultMap>
</mapper>
2.2.4. 第四步:springmvc.xml
修改health_web工程中的springmvc.xml文件,修改dubbo批量扫描的包路径
之前的包扫描
<!--批量扫描-->
<dubbo:annotation package="com.itheima.health.controller" />
现在的包扫描
<!--批量扫描-->
<dubbo:annotation package="com.itheima.health" />
注意:此处原来扫描的包为com.itheima.controller,现在改为com.itheima包的目的是需要将我们上面定义的SpringSecurityUserService也扫描到,因为在SpringSecurityUserService的loadUserByUsername方法中需要通过dubbo远程调用名称为UserService的服务。
2.2.5. 第五步:spring-security.xml
【路径】
1:定义哪些链接可以放行
2:定义哪些链接不可以放行,即需要有角色、权限才可以放行
3:认证管理,定义登录账号名和密码,并授予访问的角色、权限
4:设置在页面可以通过iframe访问受保护的页面,默认为不允许访问,需要添加security:frame-options policy=”SAMEORIGIN”
【讲解】
在health_web工程中提供spring-security.xml配置文件
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:security="http://www.springframework.org/schema/security"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security.xsd">
<!-- 静态资源(css, img, js..., login.html)-->
<security:http pattern="/css/**" security="none"/>
<security:http pattern="/img/**" security="none"/>
<security:http pattern="/js/**" security="none"/>
<security:http pattern="/plugins/**" security="none"/>
<security:http pattern="/login.html" security="none"/>
<!--
拦截规则
autoconfig userexpress
intercepter-url pattern access
登陆页面
退出登陆
关闭csrf
-->
<security:http auto-config="true" use-expressions="true">
<security:intercept-url pattern="/**" access="isAuthenticated()"/>
<security:form-login login-page="/login.html"
login-processing-url="/login.do"
username-parameter="username"
password-parameter="password"
default-target-url="/pages/main.html"
always-use-default-target="true"/>
<security:headers>
<!-- frame-options 控制页面中嵌套frame(访问其它页面,把其它页面的内容展示在这个页面上)
policy 使用的策略:
DENY: 不允许访问
SAMEORIGIN: 同域可以访问
ALLOW-FROM: 指定url可以访问
-->
<security:frame-options policy="SAMEORIGIN"/>
</security:headers>
<security:csrf disabled="true"/>
<security:logout logout-url="/logout.do" logout-success-url="/login.html" invalidate-session="true"/>
</security:http>
<!--
认证信息
认证管理器
提供者 user-service-ref springSecurityUserService implements UserDetailsService
配置加密器
-->
<security:authentication-manager>
<security:authentication-provider user-service-ref="springSecurityUserService">
<security:password-encoder ref="encoder"/>
</security:authentication-provider>
</security:authentication-manager>
<!--注册springSecurityUserService
注册密码加密器-->
<!--<bean id="springSecurityUserService" class="com.itheima.health.security.SpringSecurityUserService"/>-->
<bean id="encoder" class="org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder"/>
<!-- 权限控制注解支持 -->
<security:global-method-security pre-post-annotations="enabled"/>
</beans>
这里注意:如果出现以下问题
使用下面的配置,在spring-security.xml中添加。
放置到
<security:headers>
<!--设置在页面可以通过iframe访问受保护的页面,默认为不允许访问-->
<security:frame-options policy="SAMEORIGIN"></security:frame-options>
</security:headers>
是因为我们在main.html中定义:如果不配置springSecurity会认为iframe访问的html页面是收保护的页面,不允许访问。
<el-container>
<iframe name="right" class="el-main" src="checkitem.html" width="100%" height="580px" frameborder="0"></iframe>
</el-container>
备注:
2.2.6. 第六步:springmvc.xml
在springmvc.xml文件中引入spring-security.xml文件
<import resource="classpath:spring-security.xml"></import>
2.2.7. 第七步:CheckItemController类
在Controller的方法上加入权限控制注解,此处以CheckItemController为例
package com.itheima.health.controller;
import com.alibaba.dubbo.config.annotation.Reference;
import com.itheima.health.constant.MessageConstant;
import com.itheima.health.entity.PageResult;
import com.itheima.health.entity.QueryPageBean;
import com.itheima.health.entity.Result;
import com.itheima.health.pojo.CheckItem;
import com.itheima.health.service.CheckItemService;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
* Description: No Description
* User: Eric
*/
@RestController
@RequestMapping("/checkitem")
public class CheckItemController {
@Reference
private CheckItemService checkItemService;
@GetMapping("/findAll")
public Result findAll(){
// 调用服务查询所有的检查项
List<CheckItem> list = checkItemService.findAll();
// 封装返回的结果
return new Result(true, MessageConstant.QUERY_CHECKITEM_SUCCESS,list);
}
/**
* 新增检查项
* @param checkitem
* @return
*/
@PostMapping("/add")
@PreAuthorize("hasAuthority('CHECKITEM_ADD')")
public Result add(@RequestBody CheckItem checkitem){
// 调用业务服务
checkItemService.add(checkitem);
// 响应结果给前端
return new Result(true, MessageConstant.ADD_CHECKITEM_SUCCESS);
}
/**
* 分页查询
*/
@PostMapping("/findPage")
@PreAuthorize("hasAuthority('CHECKITEM_QUERY')")
public Result findPage(@RequestBody QueryPageBean queryPageBean){
// 调用业务来分页
PageResult<CheckItem> pageResult = checkItemService.findPage(queryPageBean);
//return pageResult;
// 返回给页面, 包装到Result, 统一风格
return new Result(true, MessageConstant.QUERY_CHECKITEM_SUCCESS,pageResult);
}
/**
* 删除
*/
@PostMapping("/deleteById")
public Result deleteById(int id){
// 调用业务服务
//try {
checkItemService.deleteById(id);
//} catch (Exception e) {
// e.printStackTrace();
//}
// 响应结果
return new Result(true, MessageConstant.DELETE_CHECKITEM_SUCCESS);
}
/**
* 通过id查询
*/
@GetMapping("/findById")
public Result findById(int id){
CheckItem checkItem = checkItemService.findById(id);
return new Result(true, MessageConstant.QUERY_CHECKITEM_SUCCESS,checkItem);
}
/**
* 修改检查项
* @param checkitem
* @return
*/
@PostMapping("/update")
public Result update(@RequestBody CheckItem checkitem){
// 调用业务服务
checkItemService.update(checkitem);
// 响应结果给前端
return new Result(true, MessageConstant.EDIT_CHECKITEM_SUCCESS);
}
}
2.2.8. 第八步:异常捕获
修改health_web项目中的HealthExceptionAdvice
package com.itheima.health.controller;
import com.itheima.health.entity.Result;
import com.itheima.health.exception.HealthException;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.InternalAuthenticationServiceException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
/**
* Description: No Description
* User: Eric
*/
// 与前端约定好的,返回的都是json数据
@RestControllerAdvice
public class HealExceptionAdvice {
/**
* 自定义招出的异常处理
* @param he
* @return
*/
@ExceptionHandler(HealthException.class)
public Result handleHealthException(HealthException he){
return new Result(false, he.getMessage());
}
/**
* 所有未知的异常
* @param he
* @return
*/
@ExceptionHandler(Exception.class)
public Result handleException(Exception he){
he.printStackTrace();
return new Result(false, "发生未知错误,操作失败,请联系管理员");
}
/**
* 密码错误
* @param he
* @return
*/
@ExceptionHandler(BadCredentialsException.class)
public Result handBadCredentialsException(BadCredentialsException he){
return handleUserPassword();
}
/**
* 用户名不存在
* @param he
* @return
*/
@ExceptionHandler(InternalAuthenticationServiceException.class)
public Result handInternalAuthenticationServiceException(InternalAuthenticationServiceException he){
return handleUserPassword();
}
private Result handleUserPassword(){
return new Result(false, "用户名或密码错误");
}
/**
* 用户名不存在
* @param he
* @return
*/
@ExceptionHandler(AccessDeniedException.class)
public Result handAccessDeniedException(AccessDeniedException he){
return new Result(false, "没有权限");
}
}
2.3. 显示用户名
【路径】
1:引入js
2:定义loginUsername属性
3:使用钩子函数,调用ajax,查询登录用户(从SpringSecurity中获取),赋值username属性
4:修改页面,使用{{loginUsername}}显示用户信息
【讲解】
前面我们已经完成了认证和授权操作,如果用户认证成功后需要在页面展示当前用户的用户名。Spring Security在认证成功后会将用户信息保存到框架提供的上下文对象中,所以此处我们就可以调用Spring Security框架提供的API获取当前用户的username并展示到页面上。
实现步骤:
第一步:在main.html页面中修改,定义username模型数据基于VUE的数据绑定展示用户名,发送ajax请求获取username
(1):引入js
<script src="../js/axios-0.18.0.js"></script>
(2):定义loginUsername属性
(3):使用钩子函数mounted,调用ajax
(4)显示用户名
页面最终如下
<!DOCTYPE html>
<html>
<head>
<!-- 页面meta -->
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>传智健康</title>
<meta name="description" content="传智健康">
<meta name="keywords" content="传智健康">
<meta content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no" name="viewport">
<!-- 引入样式 -->
<link rel="stylesheet" href="../plugins/elementui/index.css">
<link rel="stylesheet" href="../plugins/font-awesome/css/font-awesome.min.css">
<link rel="stylesheet" href="../css/style.css">
<style type="text/css">
.el-main{
position: absolute;
top: 70px;
bottom: 0px;
left: 200px;
right: 10px;
padding: 0;
}
</style>
</head>
<body class="hold-transition skin-purple sidebar-mini">
<div id="app">
<el-container>
<el-header class="main-header" style="height:70px;">
<nav class="navbar navbar-static-top" :class=''>
<!-- Logo -->
<a href="#" class="logo" style="text-align:center">
<span class="logo-lg"><img src="../img/logo.png"></span>
</a>
<div class="right-menu">
<span class="help"><i class="fa fa-exclamation-circle" aria-hidden="true"></i>帮助</span>
<el-dropdown class="avatar-container right-menu-item" trigger="click">
<div class="avatar-wrapper">
<img src="../img/user2-160x160.jpg" class="user-avatar">
{{loginUsername}}
</div>
<el-dropdown-menu slot="dropdown">
<el-dropdown-item divided>
<span style="display:block;">修改密码</span>
</el-dropdown-item>
<el-dropdown-item divided>
<span style="display:block;"><a href="/logout.do">退出</a> </span>
</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
</div>
</nav>
</el-header>
<el-container>
<el-aside width="200px">
<el-menu>
<el-submenu v-for="menu in menuList" :index="menu.path">
<template slot="title">
<i class="fa" :class="menu.icon"></i>
{{menu.title}}
</template>
<template v-for="child in menu.children">
<el-menu-item :index="child.path">
<a :href="child.linkUrl" target="right">{{child.title}}</a>
</el-menu-item>
</template>
</el-submenu>
</el-menu>
</el-aside>
<el-container>
<iframe name="right" class="el-main" src="ordersetting.html" width="100%" height="580px" frameborder="0"></iframe>
</el-container>
</el-container>
</el-container>
</div>
</body>
<!-- 引入组件库 -->
<script src="../js/vue.js"></script>
<script src="../plugins/elementui/index.js"></script>
<script type="text/javascript" src="../js/jquery.min.js"></script>
<script src="../js/axios-0.18.0.js"></script>
<script>
new Vue({
el: '#app',
data:{
loginUsername:'',
menuList:[
{
"path": "1",
"title": "工作台",
"icon":"fa-dashboard",
"children": []
},
{
"path": "2",
"title": "会员管理",
"icon":"fa-user-md",
"children": [
{
"path": "/2-1",
"title": "会员档案",
"linkUrl":"member.html",
"children":[]
},
{
"path": "/2-2",
"title": "体检上传",
"children":[]
},
{
"path": "/2-3",
"title": "会员统计",
"linkUrl":"all-item-list.html",
"children":[]
},
]
},
{
"path": "3",
"title": "预约管理",
"icon":"fa-tty",
"children": [
{
"path": "/3-1",
"title": "预约列表",
"linkUrl":"ordersettinglist.html",
"children":[]
},
{
"path": "/3-2",
"title": "预约设置",
"linkUrl":"ordersetting.html",
"children":[]
},
{
"path": "/3-3",
"title": "套餐管理",
"linkUrl":"setmeal.html",
"children":[]
},
{
"path": "/3-4",
"title": "检查组管理",
"linkUrl":"checkgroup.html",
"children":[]
},
{
"path": "/3-5",
"title": "检查项管理",
"linkUrl":"/pages/checkitem.html",
"children":[]
},
]
},
{
"path": "4",
"title": "健康评估",
"icon":"fa-stethoscope",
"children":[
{
"path": "/4-1",
"title": "中医体质辨识",
"linkUrl":"all-medical-list.html",
"children":[]
},
]
},
{
"path": "5", //菜单项所对应的路由路径
"title": "统计分析", //菜单项名称
"icon":"fa-heartbeat",
"children":[//是否有子菜单,若没有,则为[]
{
"path": "/5-1",
"title": "会员数量统计",
"linkUrl":"/pages/report_member.html",
"children":[]
},
{
"path": "/5-2",
"title": "预约套餐占比",
"linkUrl":"/pages/report_setmeal.html",
"children":[]
},
{
"path": "/5-3",
"title": "运营数据统计",
"linkUrl":"/pages/report_business.html",
"children":[]
}
]
}
]
},
mounted(){
// 获取登陆用户名
axios.get('/user/getUsername.do').then(res => {
if(res.data.flag){
this.loginUsername = res.data.data;
}else{
this.$message.error(res.data.message);
}
})
}
});
$(function() {
var wd = 200;
$(".el-main").css('width', $('body').width() - wd + 'px');
});
</script>
</html>
第二步:创建UserController并提供getUsername方法
package com.itheima.health.controller;
import com.itheima.health.constant.MessageConstant;
import com.itheima.health.entity.Result;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.User;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* Description: No Description
* User: Eric
*/
@RestController
@RequestMapping("/user")
public class UserController {
/**
* 获取登陆用户名
*/
@GetMapping("/getUsername")
public Result getUsername(){
// 获取登陆用户的认证信息
User loginUser = (User) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
// 登陆用户名
String username = loginUser.getUsername();
// 返回给前端
return new Result(true, MessageConstant.GET_USERNAME_SUCCESS,username);
}
}
通过debug调试可以看到Spring Security框架在其上下文中保存的用户相关信息:
显示当前登录人:
2.4. 用户退出
【路径】
1:在main.html中提供的退出菜单上加入超链接
2:在spring-security.xml文件中配置
【讲解】
第一步:在main.html中提供的退出菜单上加入超链接
<el-dropdown-item divided>
<span style="display:block;"><a href="/logout.do">退出</a></span>
</el-dropdown-item>
第二步:在spring-security.xml文件中配置
<!--
logout:退出登录
logout-url:退出登录操作对应的请求路径
logout-success-url:退出登录后的跳转页面
-->
<security:logout logout-url="/logout.do"
logout-success-url="/login.html" invalidate-session="true"/>
【小结】
1:导入SpringSecurity环境
(1)pom.xml中添加依赖
(2)web.xml添加代理过滤器
2:实现认证和授权
(1)导入login.html登录页面 webapp目录下
(2)认证:SpringSecurityUserService(@Component),实现UserDetailsService接口
(3)创建UserService类、UserDao接口类、UserDao映射文件(使用用户名查询当前用户信息,包括角色集合和权限集合)
(4)springmvc.xml(dubbo注解扫描范围扩大, 扫到SpringSecurityUserService)
(5)spring-security.xml(重点 存小抄 )
- 静态资源过滤
- 拦截的规则 security:http auto-config…., intercept-url, form-login, form-logout, csrf, security:header
- 开启注解支持
- 关闭跨域访问限制
- 认证管理器->提供者user-service-ref->加密器
- 加密器
(6)springmvc.xml(导入spring-security.xml)
(7)CheckItemController类(@PreAuthorize(“hasAuthority(‘CHECKITEM_ADD’)”):对类中的方法完成权限控制), hasAuthority 权限校验(t_permission.keyword), hasRole角色校验(t_role.keyword)
(8)checkitem.html(如果没有权限,可以提示错误信息)
异常捕获HealthExceptionHandler, AccessDeniedException, return 没有权限的result
3:显示用户名
从SecurityContextHolder对象中获取认证的用户信息,页面定义一个vue的data变量业接收,使用插值表达式在页面显示,页面加载时发送请求(vue created axios)
// 获取登陆用户的认证信息
User loginUser = (User) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
在jsp中获取登陆用户信息
${sessionScope.SPRING_SECURITY_CONTEXT.authentication.principal.username}
4:用户退出
调用/logout.do security帮我们做好了