springboot整合spring security(一) :身份认证,自定义用户授权管理

springboot整合spring security

通过编写自定义的SecurityConfig 类继承WebSecurityConfigurerAdapter进行安全管理

WebSecurityConfigurerAdapter是security中浏览器登录设置的主类 主要使用继承后重写以下的三个方法:

  • HttpSecurity(HTTP请求安全处理)
  • AuthenticationManagerBuilder(身份验证管理生成器)
  • WebSecurity(WEB安全)

使用UserDetailsService 身份认证

任务:访问localhost:8080/ 进入主页 点击一个详情页(未登录情况下) 跳转到登录login.html页面 输入正确的用户名和密码 登录成功并跳转到该详情页

spring security 提供了一个默认的用户登录页面login.html,以及提供了自动登录处理 (可以自定义用户登录 本篇文章下部分简述)

前端html

在这里插入图片描述

index.html

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
<head>
	<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
	<title>影视直播厅</title>
</head>
<body>
<h1 align="center">欢迎进入电影网站首页</h1>
<hr>
	<h3>普通电影</h3>
	<ul>
		<li><a th:href="@{/detail/common/1}">飞驰人生</a></li>
		<li><a th:href="@{/detail/common/2}">夏洛特烦恼</a></li>
	</ul>
	<h3>VIP专享</h3>
	<ul>
		<li><a th:href="@{/detail/vip/1}">速度与激情</a></li>
		<li><a th:href="@{/detail/vip/2}">猩球崛起</a></li>
	</ul>
</body>
</html>

1.html

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    <a th:href="@{/}">返回</a>
    <h1>飞驰人生</h1>
</body>
</html>

2.html

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    <a th:href="@{/}">返回</a>
    <h1>猩球崛起2</h1>
</body>
</html>

数据库

# 选择使用数据库
USE springbootdata;
# 创建表t_customer并插入相关数据
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);

1.导入security的启动依赖

<dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-security</artifactId>
</dependency>

2.创建Customer(用户信息)、Authority(用户权限) 两个实体类

本篇文章使用的是JPA进行数据库访问

@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 + '\'' +
                '}';
    }
}
@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 + '\'' +
                '}';
    }
}

3.创建查询接口CustomerRepository、AuthorityRepository

用于查询用户信息和权限信息

/**
 * 相当于使用JDBC认证时的
 *      定义用户查询的SQL语句时,必须返回用户名username,密码password,是否为有效用户valid 3个字段信息
 */
public interface CustomerRepository extends JpaRepository<Customer,Integer> {
    Customer findByUsername(String username);
}
/**
 * 相当于使用JDBC认证时的
 *      定义权限查询的SQL语句时,必须返回用户名username,权限authority 2个字段信息
 */
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 =?",
            nativeQuery = true)
    public List<Authority> findAuthoritiesByUsername(String username);
}

4.创建RedisConfig类

自定义Redis API模板RedisTemplate

 @Bean
    public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
        RedisTemplate<Object, Object> template = new RedisTemplate();
        template.setConnectionFactory(redisConnectionFactory);
        // 使用JSON格式序列化对象,对缓存数据key和value进行转换
        Jackson2JsonRedisSerializer jacksonSeial = new Jackson2JsonRedisSerializer(Object.class);
        // 解决查询缓存转换异常的问题
        ObjectMapper om = new ObjectMapper();
        // 指定要序列化的域,field,get和set,以及修饰符范围,ANY是都有包括private和public
        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        // 指定序列化输入的类型,类必须是非final修饰的,final修饰的类,比如String,Integer等会跑出异常
        om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        jacksonSeial.setObjectMapper(om);
        // 设置RedisTemplate模板API的序列化方式为JSON
        template.setDefaultSerializer(jacksonSeial);
        return template;
    }

自定义Redis缓存管理器RedisCacheManager,实现自定义序列化并设置缓存时效

@Bean
    public RedisCacheManager cacheManager(RedisConnectionFactory redisConnectionFactory) {
        // 分别创建String和JSON格式序列化对象,对缓存数据key和value进行转换
        RedisSerializer<String> strSerializer = new StringRedisSerializer();
        Jackson2JsonRedisSerializer jacksonSeial = new Jackson2JsonRedisSerializer(Object.class);
        // 解决查询缓存转换异常的问题
        ObjectMapper om = new ObjectMapper();
        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        jacksonSeial.setObjectMapper(om);
        // 定制缓存数据序列化方式及时效
        RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
                .entryTtl(Duration.ofDays(1))   // 设置缓存有效期为1天
                .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(strSerializer))
                .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(jacksonSeial))
                .disableCachingNullValues();   // 对空数据不进行缓存
        RedisCacheManager cacheManager = RedisCacheManager.builder(redisConnectionFactory).cacheDefaults(config).build();
        return cacheManager;
    }

5.添加CustomerService业处理类 (查询用户信息和权限)

@Service
public class CustomerService {
    @Autowired
    private CustomerRepository customerRepository;
    @Autowired
    private AuthorityRepository authorityRepository;
    @Autowired
    private RedisTemplate redisTemplate;

    /**
     * 业务控制:使用唯一用户名查询用户信息
     *  如果redis缓存中没有用户信息,就在数据库中查询,并把查询到的用户信息存入redis缓存中
     */
    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;
    }

    /**
     * 业务控制:使用唯一用户名查询用户权限
     * 如果redis缓存中没有用户权限信息,就在数据库中查询,并把查询到的用户权限信息存入redis缓存中
     * */
    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;
    }
}

6.自定义接口实现类UserDetailsServiceImpl 进行用户认证信息UserDetailsService封装

重写UserDetailsService接口的**loadUserByUsername(String s)**方法,在该方法中,使用CustomerService业务处理类获取用户的用户信息和权限信息,并通过UserDetails进行认证用户信息封装

@Service
public class UserDetailsServiceImpl implements UserDetailsService {
    @Autowired
    private CustomerService customerService;
    @Override
    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.创建SecurityConfig配置类 使用UserDetailsService进行身份认证

@EnableWebSecurity  //开启MVC security安全支持
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    private UserDetailsServiceImpl userDetailsService;
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        //  密码需要设置编码器
        BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
         //3、使用UserDetailsService进行身份认证
        auth.userDetailsService(userDetailsService).passwordEncoder(encoder);
    }
    
    **
     * 持久化Token存储
     * @return
     */
    @Bean
    public JdbcTokenRepositoryImpl tokenRepository(){
        JdbcTokenRepositoryImpl jr=new JdbcTokenRepositoryImpl();
        jr.setDataSource(dataSource);
        return jr;
    }
}

**效果:**访问localhost:8080/ 进入主页 点击一个详情页(未登录情况下) 跳转到登录login.html页面 输入正确的用户名和密码 登录成功并跳转到该详情页

自定义用户授权管理

通过在自定义的security配置类中重写WebSecurityConfigurerAdapter的configure(HttpSecurity http)方法控制用户的一些授权

HttpSecurity类的主要方法

用户请求控制相关的主要方法及说明

1.自定义用户访问控制

自定义配置类SecurityConfig,继续重写configure(HttpSecurity http)方法

	@Override
    protected void configure(HttpSecurity http) throws Exception {
              http.authorizeRequests()
                .antMatchers("/").permitAll()
                .antMatchers("/detail/common/**").hasRole("common")
                .antMatchers("/detail/vip/**").hasRole("vip")
                .anyRequest().authenticated()
                .and()
                .formLogin();        
    }

测试效果:登录common用户,只能访问common里面的页面,若访问vip里面的页面,会报403错误

2.自定义用户登录控制

spring security 提供了一个默认的用户登录页面login.html,以及提供了自动登录处理

但是通常,我们需要自定义这个登录页面

**注:**Spring Security默认使用Get方式的“/login”请求用于向登录页面跳转,默认使用Post方式的“/login”请求用于对登录后的数据进行处理。因此,自定义用户登录控制时,需要提供向用户登录页面跳转的方法,且自定义的登录页跳转路径必须与数据处理提交路径一致。

**注:登录页面中,表单提交的路径要与自定义的security配置类中的.loginProcessingUrl("路径名")相对应(原因:由于security是由UsernamePasswordAuthenticationFilter这个类定义登录的,里面默认是/login路径,我们要让他用我们的/authentication/form路径,就需要配置.loginProcessingUrl("/authentication/form")) **

表单提交的路径也要与controller层中跳转到登陆页面的请求路径一致

创建自定义的login.html

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
    <title>用户登录界面</title>
    <link th:href="@{/login/css/bootstrap.min.css}" rel="stylesheet">
    <link th:href="@{/login/css/signin.css}" rel="stylesheet">
</head>
<body class="text-center">
<form class="form-signin" th:action="@{/userLogin}" th:method="post" >
    <img class="mb-4" th:src="@{/login/img/login.jpg}" width="72px" height="72px">
    <h1 class="h3 mb-3 font-weight-normal">请登录</h1>
    <!-- 用户登录错误信息提示框 -->
    <div th:if="${param.error}"
         style="color: red;height: 40px;text-align: left;font-size: 1.1em">
        <img th:src="@{/login/img/loginError.jpg}" width="20px">用户名或密码错误,请重新登录!
    </div>
    <input type="text" name="name" class="form-control"
           placeholder="用户名" required="" autofocus="">
    <input type="password" name="pwd" class="form-control"
           placeholder="密码" required="">
    <button class="btn btn-lg btn-primary btn-block" type="submit" >登录</button>

    <div class="checkbox mb-3">
        <label>
            <input type="checkbox" name="rememberme">记住我
        </label>
    </div>
</form>
</body>
</html>

在security配置类中重写configure(HttpSecurity http)中添加

//自定义用户登录控制
        http.formLogin()
                .loginPage("/userLogin").permitAll()
                .usernameParameter("name")
                .passwordParameter("pwd")
                .defaultSuccessUrl("/")
                .failureUrl("/userLogin?error");

效果:登录页面变为自定义的登录页面,而非默认提供的

3.自定义用户退出

HttpSecurity类的logout()方法用来处理用户退出,默认路径为“/logout”的Post类型请求,同时也会清除Session和“Remeber Me”等任何默认用户配置

:Spring Boot项目中引入Spring Security框架后会自动开启CSRF防护功能,用户退出时必须使用POST请求;如果关闭了CSRF防护功能,那么可以使用任意方式的HTTP请求进行用户注销。

注:表单提交的路径要与自定义的security配置类中的.logoutUrl("路径名")相对应

退出网页的表单

<form th:action="@{/mylogout}" method="post">
        <input th:type="submit" th:value="注销" />
</form>

在security配置类中重写configure(HttpSecurity http)中添加

//自定义用户登出控制
        http.logout()
                .logoutUrl("/mylogout")
                .logoutSuccessUrl("/");

效果:登录后 点击注销按钮,退出当前账号,变为未登录状态,然后跳转到主页

4.登录用户信息获取

通过Security提供的SecurityContextHolder截获应用上下文SecurityContext,进而获取封装的用户信息

与HttpSession获取的SecurityContext不同

在controller文件中添加下面的方法

//通过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());
    }

5.记住我功能

两种实现方式:一种简单加密Token 存入浏览器的Cookie,另一种持久化到数据库

在登陆页面加入复选框

注:name的值要与security配置类中的http.rememberMe() .rememberMeParameter("rememberme")参数一致 security默认为remember-me

.tokenValiditySeconds(200) token的有效时间

<div class="checkbox mb-3">
        <label>
            <input type="checkbox" name="rememberme">记住我
        </label>
</div>
一、基于简单加密Token的方式

在security配置类中的configure方法中添加

// 定制Remember-me记住我功能
        http.rememberMe()
                .rememberMeParameter("rememberme")
                .tokenValiditySeconds(200);

效果:成功登录后,在同一浏览器内再次访问 无需登录

二、基于持久化Token的方式

1)用户成功登录后,Securith会把username、随机产生的序列号、生成的Token进行持久化存储(数据表中),并将他们的组合生成一个Cookie发送给客户端浏览器

2)用户再次访问时,首先检查客户端携带的Token,如果对应Cookie包含的username、序列号和Token与数据库中保存的一致,则通过验证并自动登录,同时系统将生成一个新的Token替换数据库中旧的Token,并将新的Cookie再次发送给客户端;若不一致,Spring Security会发现Cookie可能被盗用的情况,删除数据库中与当前用户有关的Token记录

我的理解:用户第一次登录产生一个把信息存储到数据库中,然后发送给浏览器一个Cookie ;用户再次访问,检查浏览器中的Cookie中的信息是否与数据库中的信息一致,然后生成一个新的Token更新数据库,并将新的Cookie发给浏览器。黑客可能会盗取你的Cookie,但是此时数据库中的Token已经更新了,所以盗取的Cookie中的Token与数据库中的不匹配,他就无法登录了。并且,security会判定Cookie出现被盗的情况,删除数据库中与当前用户有关的Token记录,用户就需要重新登录了。

在security配置类中添加持久化Token的方法

@Bean
    public JdbcTokenRepositoryImpl tokenRepository(){
        JdbcTokenRepositoryImpl jr=new JdbcTokenRepositoryImpl();
        jr.setDataSource(dataSource);
        return jr;
    }

在http.rememberMe()添加.tokenRepository(tokenRepository())

注:默认情况下 官方提供的存储Token的用户表是persistent_logins
在这里插入图片描述

点击注销,数据库中存储的对应Token信息会自动删除,但是,如果是Token有效期过了之后,再次登录,则会存储一个新的Token信息,原有的还在

  • 0
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值