注:实践内容参考人民邮电出版社的教程《 Spring Boot企业级开发教程》作者:黑马程序员,上传本文仅以实践过程以供大家共同学习解决问题,如有侵权不当行为,请告知后,我会更正或删除。
7.1 Spring Security介绍
7.2 Spring Security快速入门
这里通过一个设置一个网页访问,成功后添加Spring Security依赖启动器来查看效果。
步骤1:创建项目
- 1)使用Spring Initializr方式创建一个Spring Boot项目chapter07,这里将Group设置为com.itheima, Artifact设置为chapter07,Package设置 为com.itheima,在Dependencies依赖选择项中Thymeleaf依赖和Spring Web依赖。依赖选择如图,设置存储路径后按Finish,完成项目创建:
- 2)引入页面html资源文件(资源包下载:https://pan.baidu.com/s/1E01FeSOo2KcdkmnOW0EhUA),分别给templates和static文件夹导入资源,如图:
- 3)查看index.html,拟实现功能:点击“飞驰人生”,调用detail/common/1.html,其他三个链接也则链接到不同页面。资源文件已导入
<!--<div sec:authorize="hasRole('common')">-->
<h3>普通电影</h3>
<ul>
<li><a th:href="@{/detail/common/1}">飞驰人生</a></li>
<li><a th:href="@{/detail/common/2}">夏洛特烦恼</a></li>
</ul>
<!--</div>-->
<!--<div sec:authorize="hasAuthority('ROLE_vip')">-->
<h3>VIP专享</h3>
<ul>
<li><a th:href="@{/detail/vip/1}">速度与激情</a></li>
<li><a th:href="@{/detail/vip/2}">猩球崛起</a></li>
</ul>
<!--</div>-->
- 4)编写Web控制层。在项目com.itheima下创建名为controller的包,并在该包下创建一个用于页面请求的控制类FileController,代码如下:
package com.itheima.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
@Controller //步骤1:加入Controller注解
public class FileController {
@GetMapping("/detail/{type}/{path}")//步骤2:设置toDetail方法的访问路径
//步骤3:编写方法:toDetail(),注意使用@PathVariable让两个参数分别对应路径中的type和path
public String toDetail(@PathVariable("type") String type,@PathVariable("path") String path){
//步骤4:完成页面的跳转
return "/detail/"+type+"/"+path;
}
}
- 5)启动项目主程序,访问http://localhost:8080,可以访问到主页index.html,如图,通过链接,可以看到四个链接均可实现指向正确页面:
- 6)开启安全管理效果测试: 在pom.xml添加引入Spring Security依赖启动器spring-boot-starter-security:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
- 7)重启项目,可以看到控制台会自动生成一个安全密码(每次启动项目都是随机生成),访问http://localhost:8080,网址自动变为:访问http://localhost:8080/login,说明添加依赖启动器后,项目实现了Spring Security的自动化配置 ,并且具有了一些默认的安全管理功能。效果如图:
- 8)需要正确登陆,其中用户名为user,密码为控制台可查看的随机生成的密码,登陆后会跳转到index.html.至此,我们就完成了Spring Security快速入门的效果设置。
7.3 MVC Security安全配置介绍
7.4 自定义用户认证
通过定义WebSecurityConfigurerAdapter类型的Bean组件,并重写configure(AuthenticationManagerBuilder auth)方法,可以自定义用户认证。Spring Security提供了多种自定义认证方法,包括有:内存身份认证(In-Memory Authentication),JDBC身份认证(JDBC Authentication),LDAP身份认证(LDAP Authentication)、身份认证提供商(Authentication Provider)和身份详情服务(UserDatailService)。下面选取其中3个比较有代表性的方式讲解如何实现自定义用户认证。
7.4.1 内存身份认证
内存身份认证(In-Memory Authentication)是最简单的身份认证方式主要用于Security安全认证体验和测试,我们在只需要发在重写的configure(AuthentictionManagerBulider auth)方法中定义测试用户即可。
- 1)自定义WebSecurityConfigurerAdapter配置类
在项目com.itheima中创建名为config的包,并在该包下创建一个配置类:SecurityConfig,内容如图:
package com.itheima.config;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
@EnableWebSecurity //开启MVC Security安全支持,
public class SecurityConfig extends WebSecurityConfigurerAdapter {//用于MVC Security自定义配置
}
- 2)使用内存身份认证
在SecurityConfig中重写configure方法,并在该方法中使用内存身份认证的方式进行自定义用户认证。
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//设置密码编码器
BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();
//构建用户
final InMemoryUserDetailsManagerConfigurer<AuthenticationManagerBuilder> authenticationManagerBuilderInMemoryUserDetailsManagerConfigurer = auth.inMemoryAuthentication().passwordEncoder(bCryptPasswordEncoder);
//模拟两用户,设置用户名withUser,设置密码password,引用密码编码器加密,设置用户权限roles()
authenticationManagerBuilderInMemoryUserDetailsManagerConfigurer.withUser("shitou").password(bCryptPasswordEncoder.encode("123")).roles("comment");
authenticationManagerBuilderInMemoryUserDetailsManagerConfigurer.withUser("李四").password(bCryptPasswordEncoder.encode("321")).roles("vip");
}
- 3)效果测试,重启项目,查看到控制台没有随机生成的密码了。访问http://localhost:8080,开始测试。用户名输错效果如图
用户名输入正确跳转到index.html,可以查看四个影片链接。
7.4.2 JDBC身份认证
JDBC身份认证(JDBC Authentication)是通过JDBC连接数据库对已有用户身份进行认证,下面通过一个案例讲解:
- 数据准备
- (1)启动之前创建的名为springbootdata的数据库
- (2)在该数据库中创建 3个表t_customer(用户表)、t_authority(角色权限表)和t_customer_authority(中间表:用户对应的角色权限),并预先插入几条测试数据。对应的SQL语句:
# 选择使用数据库
USE springbootdata;
# 创建表t_customer并插入相关数据,密码为123456
DROP TABLE IF EXISTS `t_customer`;
CREATE TABLE `t_customer` (
`id` int(20) NOT NULL AUTO_INCREMENT,
`username` varchar(200) DEFAULT NULL,
`password` varchar(200) DEFAULT NULL,
`valid` tinyint(1) NOT NULL DEFAULT '1',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8;
INSERT INTO `t_customer` VALUES ('1', 'shitou', '$2a$10$5ooQI8dir8jv0/gCa1Six.GpzAdIPf6pMqdminZ/3ijYzivCyPlfK', '1');
INSERT INTO `t_customer` VALUES ('2', '李四', '$2a$10$5ooQI8dir8jv0/gCa1Six.GpzAdIPf6pMqdminZ/3ijYzivCyPlfK', '1');
# 创建表t_authority并插入相关数据
DROP TABLE IF EXISTS `t_authority`;
CREATE TABLE `t_authority` (
`id` int(20) NOT NULL AUTO_INCREMENT,
`authority` varchar(20) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8;
INSERT INTO `t_authority` VALUES ('1', 'ROLE_common');
INSERT INTO `t_authority` VALUES ('2', 'ROLE_vip');
# 创建表t_customer_authority并插入相关数据
DROP TABLE IF EXISTS `t_customer_authority`;
CREATE TABLE `t_customer_authority` (
`id` int(20) NOT NULL AUTO_INCREMENT,
`customer_id` int(20) DEFAULT NULL,
`authority_id` int(20) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8;
INSERT INTO `t_customer_authority` VALUES ('1', '1', '1');
INSERT INTO `t_customer_authority` VALUES ('2', '2', '2');
# 记住我功能中创建持久化Token存储的数据表
create table persistent_logins (username varchar(64) not null,
series varchar(64) primary key,
token varchar(64) not null,
last_used timestamp not null);
- (3)使用SQLyog运行效果 如图:
- 在pom.xml中添加JDBC连接数据库的依赖启动器
<!--JDBC数据库连接启动器-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<!--MySQL数据加接驱动-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
- 在全局配置application.properties文件进行数据库连接配置
spring.datasource.url=jdbc:mysql://localhost:3306/springbootdata?serverTimezone=UTC
spring.datasource.username=root
spring.datasource.password=root
- 4)使用JDBC进行身份认证
在以上准备工作完成后,在configure方法中使用JDBC身份认证的方式 进行自定义用户认证。SecurityConfig.java内容 如下:
package com.itheima.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.authentication.configurers.provisioning.InMemoryUserDetailsManagerConfigurer;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import javax.sql.DataSource;
@EnableWebSecurity //开启MVC Security安全支持,
public class SecurityConfig extends WebSecurityConfigurerAdapter {//用于MVC Security自定义配置
@Autowired
private DataSource dataSource;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//设置密码编码器,从Spring Security 5开始,自定义用户认证必须设置密码编码器用于保护密码,并提供了多种密码编码器
BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();
/*注释内存身份认证//构建用户
final InMemoryUserDetailsManagerConfigurer<AuthenticationManagerBuilder> authenticationManagerBuilderInMemoryUserDetailsManagerConfigurer = auth.inMemoryAuthentication().passwordEncoder(bCryptPasswordEncoder);
//模拟两用户,设置用户名withUser,设置密码password,引用密码编码器加密,设置用户权限roles()
authenticationManagerBuilderInMemoryUserDetailsManagerConfigurer.withUser("shitou").password(bCryptPasswordEncoder.encode("123")).roles("comment");
authenticationManagerBuilderInMemoryUserDetailsManagerConfigurer.withUser("李四").password(bCryptPasswordEncoder.encode("321")).roles("vip");
*/
//使用JDBC进行身份认证
String userSQL ="select username,password,valid from t_customer where username = ?";
String authoritySQL="select c.username,a.authority from t_customer c,t_authority a,t_customer_authority ca where ca.customer_id=c.id and ca.authority_id=a.id and c.username =?";
auth.jdbcAuthentication().passwordEncoder(bCryptPasswordEncoder).dataSource(dataSource).usersByUsernameQuery(userSQL).authoritiesByUsernameQuery(authoritySQL);
}
}
- 5)重启项目,效果测试,当访问 http://localhost:8080/,要求输入用户名和密码,需要输入与数据库的一致,测试可以看到,当输入shitou,密码:123456时,正常进入index.html页面。
7.4.3 UserDetailsService身份认证
- 1.准备工作1:pom.xml的dependency设置添加三个Security与Thymeleaf整合实现前端页面安全访问控制,Redis缓存启动器和Spring Data JPA操作数据库
<!-- Security与Thymeleaf整合实现前端页面安全访问控制 -->
<dependency>
<groupId>org.thymeleaf.extras</groupId>
<artifactId>thymeleaf-extras-springsecurity5</artifactId>
</dependency>
<!-- Redis缓存启动器-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- Spring Data JPA操作数据库 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
- 2.准备工作2:在com.itheima创建包service,创建定义查询用户及角色信息的服务接口,实现对用户数据结合Redis缓存进行业务处理,CustomerService.java内容如下:
package com.itheima.service;
import com.itheima.domain.Customer;
import com.itheima.domain.Authority;
import com.itheima.repository.CustomerRepository;
import com.itheima.repository.AuthorityRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class CustomerService {
@Autowired
private CustomerRepository customerRepository;
@Autowired
private AuthorityRepository authorityRepository;
@Autowired
private RedisTemplate redisTemplate;
// 业务控制:使用唯一用户名查询用户信息
public Customer getCustomer(String username){
Customer customer=null;
Object o = redisTemplate.opsForValue().get("customer_"+username);
if(o!=null){
customer=(Customer)o;
}else {
customer = customerRepository.findByUsername(username);
if(customer!=null){
redisTemplate.opsForValue().set("customer_"+username,customer);
}
}
return customer;
}
// 业务控制:使用唯一用户名查询用户权限
public List<Authority> getCustomerAuthority(String username){
List<Authority> authorities=null;
Object o = redisTemplate.opsForValue().get("authorities_"+username);
if(o!=null){
authorities=(List<Authority>)o;
}else {
authorities=authorityRepository.findAuthoritiesByUsername(username);
if(authorities.size()>0){
redisTemplate.opsForValue().set("authorities_"+username,authorities);
}
}
return authorities;
}
}
- 3.准备工作3:对于CustomerService.java中引用的CustomerRepository和AuthorityRepository,我们在com.itheima下新建repository包,并分别建立这两个类。CustomerRepository.java代码:
package com.itheima.repository;
import com.itheima.domain.Customer;
import org.springframework.data.jpa.repository.JpaRepository;
public interface CustomerRepository extends JpaRepository<Customer,Integer> {
Customer findByUsername(String username);
}
AuthorityRepository.java代码:
package com.itheima.repository;
import com.itheima.domain.Authority;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import java.util.List;
public interface AuthorityRepository extends JpaRepository<Authority,Integer> {
@Query(value = "select a.* from t_customer c,t_authority a,t_customer_authority ca where ca.customer_id=c.id and ca.authority_id=a.id and c.username =?1",nativeQuery = true)
public List<Authority> findAuthoritiesByUsername(String username);
}
- 4.准备工作4:由于reposity中用到实体类Customer和Authority,我们在com.itheima下建立包domain,并在包下建立实体类Customer和Authority,Customer.java代码:
package com.itheima.domain;
import javax.persistence.*;
@Entity(name = "t_customer")
public class Customer {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
private String username;
private String password;
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 getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
@Override
public String toString() {
return "Customer{" +
"id=" + id +
", username='" + username + '\'' +
", password=" + password +
'}';
}
}
Authority.java代码:
package com.itheima.domain;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
@Entity(name = "t_authority ")
public class Authority {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
private String authority ;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getAuthority() {
return authority;
}
public void setAuthority(String authority) {
this.authority = authority;
}
@Override
public String toString() {
return "Authority{" +
"id=" + id +
", authority='" + authority + '\'' +
'}';
}
}
- 5.准备工作5:运行redis服务程序:redis-server.exe,在application.properties添加redis连接
spring.redis.host=127.0.0.1
spring.redis.port=6379
spring.redis.password=
- 6.在service包中自定义一个UserDetailsService接口实现类进行用户认证信息封装,UserDetailsServiceImpl.java内容如下:
package com.itheima.service;
import com.itheima.domain.Authority;
import com.itheima.domain.Customer;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.*;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.stream.Collectors;
/**
* @Classname UserDetailsServiceImpl
* @Description 自定义一个UserDetailsService接口实现类进行用户认证信息封装
* @Date 2019-3-5 16:08
* @Created by CrazyStone
*/
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
private CustomerService customerService;
@Override
//重写loadUserByUsername方法用于借助CustomerService业务处理类获取用户信息和权限信息,并通过UserDetails进行认证用户信息封装
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
// 通过业务方法获取用户及权限信息
Customer customer = customerService.getCustomer(s);
List<Authority> authorities = customerService.getCustomerAuthority(s);
// 对用户权限进行封装
List<SimpleGrantedAuthority> list = authorities.stream().map(authority -> new SimpleGrantedAuthority(authority.getAuthority())).collect(Collectors.toList());
// 返回封装的UserDetails用户详情类
if(customer!=null){
UserDetails userDetails= new User(customer.getUsername(),customer.getPassword(),list);
return userDetails;
} else {
// 如果查询的用户不存在(用户名不存在),必须抛出此异常
throw new UsernameNotFoundException("当前用户不存在!");
}
}
}
- 7.在configure方法中(先注释掉JDBC身份认证方式),使用UserDetailsService身份认证进行自定义用户认证:
@EnableWebSecurity //开启MVC Security安全支持,
public class SecurityConfig extends WebSecurityConfigurerAdapter {//用于MVC Security自定义配置
@Autowired
private DataSource dataSource;
@Autowired
private UserDetailsServiceImpl userDetailsService;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//设置密码编码器,从Spring Security 5开始,自定义用户认证必须设置密码编码器用于保护密码,并提供了多种密码编码器
BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();
/*//构建用户
final InMemoryUserDetailsManagerConfigurer<AuthenticationManagerBuilder> authenticationManagerBuilderInMemoryUserDetailsManagerConfigurer = auth.inMemoryAuthentication().passwordEncoder(bCryptPasswordEncoder);
//模拟两用户,设置用户名withUser,设置密码password,引用密码编码器加密,设置用户权限roles()
authenticationManagerBuilderInMemoryUserDetailsManagerConfigurer.withUser("shitou").password(bCryptPasswordEncoder.encode("123")).roles("comment");
authenticationManagerBuilderInMemoryUserDetailsManagerConfigurer.withUser("李四").password(bCryptPasswordEncoder.encode("321")).roles("vip");
*/
//使用JDBC进行身份认证
/* // 2、使用JDBC进行身份认证
String userSQL ="select username,password,valid from t_customer where username = ?";//通过查询获得用户名
String authoritySQL="select c.username,a.authority from t_customer c,t_authority a,t_customer_authority ca where ca.customer_id=c.id and ca.authority_id=a.id and c.username =?";
auth.jdbcAuthentication().passwordEncoder(bCryptPasswordEncoder).dataSource(dataSource).usersByUsernameQuery(userSQL).authoritiesByUsernameQuery(authoritySQL);
*/
//使用UserDetailsService身份认证
auth.userDetailsService(userDetailsService).passwordEncoder(bCryptPasswordEncoder);
}
}
- 8.重启项目,通过浏览器访问:http://localhost:8080,进行测试,
使用账号和密码登陆,出现如图所示错误,因涉及缓存,需要进行序列化:
这里提示的是Customer没有实现序列化,修改实体类Customer.java,添加implements Serializable,实现序列化,如图:
public class Customer implements Serializable {//注释序列化
同时对另一实体类Authority也实现序列化,如图:
public class Authority implements Serializable {//注意序列化
重启项目测试:
错误用户名登陆:
正确用户名如shitou,密码:123456登陆:
至此,完成UserDetailsService身份认证实践。
7.5 自定义用户授权管理
从上面的案例演示中,我们看到可以实现不同权限的用户访问不同的效果或页面,接下来,我们针对Web应用中常见的自定义用户授权管理进行介绍:
7.5.1 自定义用户访问控制
演示:
- 1.在SecurityConfig.java中重写configure(HttpSecurity http)方法,代码如下:
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().antMatchers("/").permitAll()//对“/”路径开头的不拦截
.antMatchers("/detail/common/**").hasRole("common")//对路径为"/detail/common/"开头的进行拦截,角色为common的不拦截
.antMatchers("detail/vip/**").hasRole("vip")//对路径为"/detail/vip/"开头的进行拦截,角色为vip的不拦截
.anyRequest().authenticated()//除去以上的3个映射路径以外,其他的均要求登录
.and().formLogin();//配置默认登陆页面
}
- 2.停止之前的项目后,重新启动项目启动类。浏览器访问:http://localhost:8080/,因为允许访问,效果如图:
使用用户名:shitou,密码:123456,对应的权限:ROLE_common,登陆效果如图:
访问“飞驰人生”链接,效果如图:
7.5.2 自定义用户登录
演示实践:
- 1.自定义用户登陆界面:这里我们导入相关的静态资源,请确保login和static文件夹已导入。如图:
- 2.自定义用户登录跳转
在之前的FileController类中添加一个跳转到登录页面login.html的方法
@GetMapping("/userLogin")//与login.html中的表单跳转th:action="@{/userLogin}的路径一致
public String toLoginPage(){
return "/login/login";//跳转到静态资源login文件夹的login.html页面
}
- 3.自定义用户登录控制
打开SecurityConfig类,重写configure方法:
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().antMatchers("/").permitAll()//对“/”路径开头的不拦截
// 需要对static文件夹下login文件夹对应的静态资源进行统一放行
.antMatchers("/login/**").permitAll()
.antMatchers("/detail/common/**").hasRole("common")//对路径为"/detail/common/"开头的进行拦截,角色为common的不拦截
.antMatchers("detail/vip/**").hasRole("vip")//对路径为"/detail/vip/"开头的进行拦截,角色为vip的不拦截
.anyRequest().authenticated()//除去以上的3个映射路径以外,其他的均要求登录
.and().formLogin();//配置默认登陆页面
// 自定义用户登录控制
http.formLogin()
.loginPage("/userLogin").permitAll()//配置登陆路径,跟页面中的设置一致
.usernameParameter("name").passwordParameter("pwd")//接收前台传递的用户名写密码,跟login.html页面的保持一致
.defaultSuccessUrl("/")//登陆成功跳转到首页
.failureUrl("/userLogin?error");//登陆失败返回登陆页面参数为?error
}
- 重启项目,浏览器访问:http://localhost:8080,效果测试:
1)首页要求登录:
2)点击"请登录",输入错误账号密码,效果如图。注意网址变化:
3)用shitou,密码:123456登陆,效果跟前面的登陆一样。
7.5.3 自定义用户退出
- 1.添加自定义用户退出链接,这部分体现在index.html页面
<form th:action="@{/mylogout}" method="post">
<input th:type="submit" th:value="注销" />
</form>
- 2.自定义用户退出控制
在SecurityConfig类中添加自定义用户退出控制方法:
// 自定义用户退出控制
http.logout()
.logoutUrl("/mylogout")//跟index.html"注销"表单中的路径一致
.logoutSuccessUrl("/");//注销成功后的跳转页面为根目录的首页
- 3.重启服务,测试注销是否正常使用。
7.5.4 登录用户信息获取
- 使用HttpSession获取用户信息,在FilmController控制类中新增一个用于获取当前会话用户信息的getUser()方法,代码如下:
/**
* 通过传统的HttpSession获取Security控制的登录用户信息
*/
@GetMapping("/getuserBySession")
@ResponseBody
public void getUser(HttpSession session) {
// 从当前HttpSession获取绑定到此会话的所有对象的名称
Enumeration<String> names = session.getAttributeNames();
while (names.hasMoreElements()){
// 遍历获取HttpSession中会话名称
String element = names.nextElement();
// 获取HttpSession中的应用上下文
SecurityContextImpl attribute = (SecurityContextImpl) session.getAttribute(element);//默认返回一个Object对象,本质是SecurityContextImpl类,所以进行强制转换
System.out.println("element: "+element);
System.out.println("attribute: "+attribute);
// 获取用户相关信息
Authentication authentication = attribute.getAuthentication();
UserDetails principal = (UserDetails)authentication.getPrincipal();//默认返回一个Object对象,本质是UserDetails类,进行强制转换,其中包括有用户名、密码、权限、是否过期等
System.out.println(principal);
System.out.println("username: "+principal.getUsername());
}
}
- 2.使用SecurityContextHolder获取用户信息
Spring Security针对拦截的登录用户专门提供了一个SecurityContextHolder类,来获取Spring Security的应用上下文SecurityContex,进而获取封装的用户信息。在FilmController控制类中新增一个获取当前会话用户信息的getUser2()方法,代码如下:
/**
* 通过Security提供的SecurityContextHolder获取登录用户信息
*/
@GetMapping("/getuserByContext")
@ResponseBody
public void getUser2() {
// 获取应用上下文
SecurityContext context = SecurityContextHolder.getContext();
System.out.println("userDetails: "+context);
// 获取用户相关信息
Authentication authentication = context.getAuthentication();
UserDetails principal = (UserDetails)authentication.getPrincipal();
System.out.println(principal);
System.out.println("username: "+principal.getUsername());
}
- 3.重启项目,浏览器运行http://localhost:8080,使用用户名shitou和密码:123456进行登陆,如图:
访问http://localhost:8080/getuserByContext,效果如图
以Debug模式启动项目(application.properties中添加一句debug=true,或命令行java –jar xxx.jar --debug,重启项目即可),在登陆用户后同一浏览器访问http://localhost:8080/getuserBySession,则在控制台可以查看到效果如图:
7.5.5 记住我功能
- 1.基于简单加密Token的方式
- 1)login.html页面新增一个记住我的复选框,导入的静态资源已存在,代码如下:
<div class="checkbox mb-3">
<label>
<input type="checkbox" name="rememberme"> 记住我
</label>
</div>
- 2)打开SecurityConfig类,重写configure(HttpSecurity http)方法进行记住 我的功能配置。代码如下:
// 定制Remember-me记住我功能
http.rememberMe()
.rememberMeParameter("rememberme")
.tokenValiditySeconds(200);
- 重启项目,浏览器访问http://localhost:8080,并登陆,勾选“记住我”:
访问http://localhost:8080/detail/common/1正常,关闭浏览器再打开,直接访问http://localhost:8080/detail/common/1,仍正常访问,说明cookie起作用
- 2.基于持久化Token的方式
1)在数据库中对记住我功能中创建持久化Token存储的数据表
# 记住我功能中创建持久化Token存储的数据表
CREATE TABLE persistent_logins (username VARCHAR(64) NOT NULL,
series VARCHAR(64) PRIMARY KEY,
token VARCHAR(64) NOT NULL,
last_used TIMESTAMP NOT NULL);
效果如图:
- 2)打开SecurityConfig类,重写configure(HttpSecurity http)方法中的进行记住我的功能。代码如下:
// 定制Remember-me记住我功能
http.rememberMe()
.rememberMeParameter("rememberme")
.tokenValiditySeconds(200)
// 对cookie信息进行持久化管理
.tokenRepository(tokenRepository());
}
新增一个持久化Token存储方法tokenRepository(),代码如下:
/**
* 持久化Token存储
*/
@Bean
public JdbcTokenRepositoryImpl tokenRepository(){
JdbcTokenRepositoryImpl jr=new JdbcTokenRepositoryImpl();
jr.setDataSource(dataSource);
return jr;
}
- 重启项目,测试过程类似上面。效果如图:
可以看到,数据表实现持久化Token存储。
- 7.5.6 CSRF防护功能
- 1.CSRF防护功能关闭
- 1)在resources/templates目录中创建一个名为csrf的文件夹,并创建模拟修改用户账号信息的Thymeleaf页面csrfTest.html用来进行CSRF测试,内容如下:
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>用户修改</title>
</head>
<body>
<div align="center">
<form method="post" th:action="@{/updateUser}">
<!-- <input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}"/>-->
用户名: <input type="text" name="username" /><br />
密 码: <input type="password" name="password" /><br />
<button type="submit">修改</button>
</form>
</div>
</body>
</html>
- 2)编写后台控制层方法,在controller的包下,创建一个用于CSRF页面请求测试的控制类CSRFController,内容如下:
package com.itheima.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;
import javax.servlet.http.HttpServletRequest;
@Controller
public class CSRFController {
//向用户修改页面跳转
@GetMapping("/toUpdate")
public String toUpdate(){
return "csrf/csrfTest";
}
//接收页面请求的方法
@ResponseBody
@PostMapping("/updateUser")
public String updateUse(@RequestParam String username,@RequestParam String password,@RequestParam HttpServletRequest request){
System.out.println("username:"+username);
System.out.println("password:"+password);
return "ok";
}
}
- 3)重启项目,浏览器访问:localhost:8080//toUpdate测试,需要登录,使用李四,密码123456,登录后再访问,效果如图:
- 4)点击修改,因默认的CSRF防护原因,访问失败。
- 5)需要关闭有两方法:(1)在SecurityConfig的configure(HttpSecurity http)方法中添加一行代码关闭,但不建议这样操作,安全性会降低:
// 可以关闭Spring Security默认开启的CSRF防护功能
// http.csrf().disable();
(2)在csrfTest.html中添加隐藏域来携带Security提供的CSRF Token信息,其中,th:name="${_csrf.parameterName}" 会获取Security默认提供的CSRF Token对应的key值,
th:value="${_csrf.token}"会获取Security默认随机生成的CSRF Token对应的value值。
<form method="post" th:action="@{/updateUser}">
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}"/>
用户名: <input type="text" name="username" /><br />
密 码: <input type="password" name="password" /><br />
<button type="submit">修改</button>
</form>
重启项目,浏览器访问http://localhost:8080/toUpdate,使用账号:李四,密码:123456,登录后如图:
点击修改报错,如图:
request参数默认为必传参数,对CSRFController中的updateUser方法头部修正如下:
```
public String updateUser(@RequestParam String username,@RequestParam String password, @RequestParam(required = false) HttpServletRequest request){
```
重启项目,再行测试,点修改时会返回OK
- 针对Ajax类型的数据修改请求请参考课本:
7.6 Security管理前端页面
- 1.添加thymeleaf-extras-springsecurity5依赖启动器
<!-- Security与Thymeleaf整合实现前端页面安全访问控制 -->
<dependency>
<groupId>org.thymeleaf.extras</groupId>
<artifactId>thymeleaf-extras-springsecurity5</artifactId>
</dependency>
- 2.修改前端页面,使用Security相关标签进行页面控制
打开index.html,引入Security安全标签,并在页面中根据需要使用Security标签进行显示控制,修改后的index.html内容如下
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org" xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity5">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>影视直播厅</title>
</head>
<body>
<h1 align="center">欢迎进入电影网站首页</h1>
<div sec:authorize="isAnonymous()"><!--判断是否是匿名用户,如果是,请登录-->
<h2 align="center">游客您好,如果想查看电影<a th:href="@{/userLogin}">请登录</a></h2>
</div>
<div sec:authorize="isAuthenticated()"><!--判断是否是认证了的用户,认证的用户才展示权限及注销功能-->
<h2 align="center"><span sec:authentication="name" style="color: #007bff"></span>您好,您的用户权限为<span sec:authentication="principal.authorities" style="color:darkkhaki"></span>,您有权观看以下电影</h2>
<form th:action="@{/mylogout}" method="post">
<input th:type="submit" th:value="注销" />
</form>
</div>
<hr>
<div sec:authorize="hasRole('common')"><!--判断用户权限是否是普通权限-->
<h3>普通电影</h3>
<ul>
<li><a th:href="@{/detail/common/1}">飞驰人生</a></li>
<li><a th:href="@{/detail/common/2}">夏洛特烦恼</a></li>
</ul>
</div>
<div sec:authorize="hasAuthority('ROLE_vip')"><!--判断用户权限是否是VIP权限-->
<h3>VIP专享</h3>
<ul>
<li><a th:href="@{/detail/vip/1}">速度与激情</a></li>
<li><a th:href="@{/detail/vip/2}">猩球崛起</a></li>
</ul>
</div>
</body>
</html>
- 3.重启服务器,可以看到不同权限用户显示不同的电影。
普通游客:
普通用户:
VIP用户: