Spring SecurityJava编程社区JAVA

SpringBoot+Security

2019-08-12  本文已影响54人  盼旺

测试环境

springboot 2.1.7
security 5.1.6
jdk 1.8
mysql 8.0

Security介绍

Spring Security是一个能够为基于Spring的企业应用系统提供声明式的安全访问控制解决方案的安全框架。它提供了一组可以在Spring应用上下文中配置的Bean,充分利用了Spring的IOC,DI,AOP(面向切面编程)功能,为应用系统提供声明式的安全访问控制功能,减少了为安全控制编写大量重复代码的工作。

中文参考手册

https://springcloud.cc/spring-security.html

核心API

Spring Security的主要构建块

//能看到明显使用ThreadLocal线程就可以了
private static void initialize() {
    if (!StringUtils.hasText(strategyName)) {
        strategyName = "MODE_THREADLOCAL";
    }
    if (strategyName.equals("MODE_THREADLOCAL")) {
        strategy = new ThreadLocalSecurityContextHolderStrategy();
    } else if (strategyName.equals("MODE_INHERITABLETHREADLOCAL")) {
        strategy = new InheritableThreadLocalSecurityContextHolderStrategy();
    } else if (strategyName.equals("MODE_GLOBAL")) {
        strategy = new GlobalSecurityContextHolderStrategy();
    } else {
        try {
            Class<?> clazz = Class.forName(strategyName);
            Constructor<?> customStrategy = clazz.getConstructor();
            strategy = (SecurityContextHolderStrategy)customStrategy.newInstance();
        } catch (Exception var2) {
            ReflectionUtils.handleReflectionException(var2);
        }
    }
    ++initializeCount;
}
public interface Authentication extends Principal, Serializable {
    Collection<? extends GrantedAuthority> getAuthorities();
    Object getCredentials();
    Object getDetails();
    Object getPrincipal();
#是否被认证,认证为true  
    boolean isAuthenticated();
#设置是否能被认证
    void setAuthenticated(boolean var1) throws IllegalArgumentException;
}
1)、getAuthorities,权限列表,通常是代表权限的字符串集合;
2)、getCredentials,密码,认证之后会移出,来保证安全性;
3)、getDetails,请求的细节参数;
4)、getPrincipal, 核心身份信息,一般返回UserDetails的实现类。
public interface UserDetails extends Serializable {
 #1.权限集合
 Collection<? extends GrantedAuthority> getAuthorities();
 #2.密码  
 String getPassword();
 #3.用户民
 String getUsername();
 #4.用户是否过期
 boolean isAccountNonExpired();
 #5.是否锁定    
 boolean isAccountNonLocked();
 #6.用户密码是否过期    
 boolean isCredentialsNonExpired();
 #7.账号是否可用(可理解为是否删除)
 boolean isEnabled();
}
public interface UserDetailsService {
    UserDetails loadUserByUsername(String var1) throws UsernameNotFoundException;
}

认证流程顶级接口。该对象提供了认证方法的入口,接收一个Authentiaton对象作为参数;可以通过实现AuthenticationManager接口来自定义自己的认证方式,Spring提供了一个默认的实现,ProviderManager

public interface AuthenticationManager {
    Authentication authenticate(Authentication var1) throws AuthenticationException;
}

总结

UserDetailsService接口作为桥梁,是DaoAuthenticationProvier与特定用户信息来源进行解耦的地方,UserDetailsServiceUserDetailsUserDetailsManager所构成;UserDetailsUserDetailsManager各司其责,一个是对基本用户信息进行封装,一个是对基本用户信息进行管理;
特别注意,UserDetailsServiceUserDetails以及UserDetailsManager都是可被用户自定义的扩展点,我们可以继承这些接口提供自己的读取用户来源和管理用户的方法,比如我们可以自己实现一个 与特定 ORM 框架,比如 Mybatis或者 Hibernate,相关的UserDetailsServiceUserDetailsManager

Demo例子

项目实现的流程描述

1)、三个页面分类,page1、page2、page3
2)、未登录授权都不可以访问
3)、登录后根据用户权限,访问指定页面
4)、对于未授权页面,访问返回403:资源不可用
5)、记住密码后,重启项目不需要登陆

项目中大部分都有注释,还有一些地方不知道为什么这么做的可以百度一下;
比如:security记住密码功能实现 相信度娘有很多的,可以告诉你

先看效果
数据库中的表是JPA自动生成的,不要自己建表哦 测试数据是添加的



security.gif
当匿名用户(未登录状态)访问wg1会直接跳转到登陆界面,登陆root后因为它有访问wg1和wg3的权限 然后没有wg2的权限所以返回403 然后注销跳转到主页最开始的界面
记住密码演示看最后 就是重启项目6分钟内不用登陆了

\color{red}{总览项目结构图} 总览项目结构图*

新建项目

核心依赖配置
Druid的配置参考:https://www.jianshu.com/p/c3006ac7e37c

server.port=8080

# ==============================
# MySQL connection config
# ==============================
spring.datasource.url=jdbc:mysql://localhost:3306/spring_demo?useUnicode=true&characeterEncoding=utf-8&serverTimezone=UTC
spring.datasource.username=root
spring.datasource.password=123456
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.type=com.alibaba.druid.pool.DruidDataSource  
#Druid会自动跟url识别驱动类名,如果连接的数据库非常见数据库,配置属性driverClassName
# ==============================
# Druid 数据源专用配置
# ==============================
# 初始化大小,最小,最大
spring.datasource.initialSize=3
spring.datasource.minIdle=5
spring.datasource.maxActive=20
# 配置获取连接等待超时的时间
spring.datasource.maxWait=30000
# 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
spring.datasource.timeBetweenEvictionRunsMillis=60000
# 配置一个连接在池中最小生存的时间,单位是毫秒
spring.datasource.minEvictableIdleTimeMillis=300000
spring.datasource.maxEvictableIdleTimeMillis=900000
spring.datasource.validationQuery=SELECT 1 FROM DUAL
spring.datasource.testWhileIdle=true
spring.datasource.testOnBorrow=false
spring.datasource.testOnReturn=false
# 打开PSCache,并且指定每个连接上PSCache的大小
spring.datasource.poolPreparedStatements=true
spring.datasource.maxPoolPreparedStatementPerConnectionSize=20
# 配置监控统计拦截的filters,去掉后监控界面sql无法统计,'wall'用于防火墙
spring.datasource.filters=stat,wall,log4j
# 通过connectProperties属性来打开mergeSql功能;慢SQL记录
# 合并多个DruidDataSource的监控数据
#spring.datasource.useGlobalDataSourceStat=true
# 通过connectProperties属性来打开mergeSql功能;慢SQL记录
spring.datasource.connectionProperties=druid.stat.mergeSql=true;druid.stat.slowSqlMillis=10000
# ==============================
# Thymeleaf configurations
# ==============================
spring.thymeleaf.mode=HTML
spring.thymeleaf.cache=false
spring.thymeleaf.servlet.content-type=text/html
spring.thymeleaf.encoding=UTF-8
# ==============================
# jpa configurations
# ==============================
#配置指明在程序启动的时候要删除并且创建实体类对应的表。这个参数很危险,
#因为他会把对应的表删除掉然后重建。所以千万不要在生成环境中使用。只有在测试环境中,一开始初始化数据库结构的时候才能使用一次。
#过后使用update
#spring.jpa.hibernate.ddl-auto=create
spring.jpa.hibernate.ddl-auto=update
# 配置在日志中打印出执行的 SQL 语句信息。
#spring.jpa.show-sql=true
#默认的存储引擎切换为 InnoDB
spring.jpa.database-platform=org.hibernate.dialect.MySQL57InnoDBDialect

启动类添加如下可以不启动security这里当然不是去设置不启动

@EnableAutoConfiguration(exclude = {
        org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration.class
})

security核心配置


SecurityConfig
package com.wg.securitydemo.config;

import com.wg.securitydemo.service.CustomPasswordEncoder;
import com.wg.securitydemo.service.UserDetailServiceImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.web.authentication.rememberme.JdbcTokenRepositoryImpl;
import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository;

import javax.sql.DataSource;

/**
 * EnableWebSecurity注解使得SpringMVC集成了Spring Security的web安全支持
 */
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    /**
     * 权限配置
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
//        super.configure(http);
//        禁用CSRF
             http.csrf().disable();
        // 配置拦截规则
             http
                .authorizeRequests()
                     .antMatchers("/").permitAll()//index.html页面不需要验证
                     .antMatchers("/page1/**").hasRole("LEVEL1")////数据库角色表的角色码必须加ROLE_开头,ROLE_LEVEL1
                     .antMatchers("/page2/**").hasRole("LEVEL2")
                     .antMatchers("/page3/**").hasRole("LEVEL3")
                     .anyRequest().authenticated();//任何尚未匹配的URL只需要对用户进行身份验证
        // 配置登录功能
             http
                 .formLogin().loginPage("/login")//更新的配置指定登录页面的位置
                             .usernameParameter("user")//取得表单中name为user的信息
                             .passwordParameter("pwd")//取得表单中name为pwd的信息
                             .successForwardUrl("/")
                             .failureForwardUrl("/403")
                             .permitAll();
        // 注销成功跳转首页
             http.logout().logoutUrl("/logout")//指定注销的页面url 你指定后 他就会自动注销了
                     .logoutSuccessUrl("/");
        //开启记住我功能
             http.rememberMe()
                     .rememberMeParameter("remeber")//取得表单中name为remeber的信息
                     .tokenRepository(persistentTokenRepository())//调用下面的记住密码的功能函数
                     .tokenValiditySeconds(360)// 失效时间以秒为单位
                     .userDetailsService(userDetailsService);
    }
    /**
     * 自定义认证数据源
     */
    @Autowired
    UserDetailServiceImpl userDetailsService;
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//        super.configure(auth);
        auth.userDetailsService(userDetailsService)//调用用户认证的类
                .passwordEncoder(new CustomPasswordEncoder());//这里掉用的是自定义密码加密方式  好像密码加密方式必须要有 这里可以写个直接返回原密码的加密类
    }
    /**
     *记住密码的功能
     * */
    @Autowired
    private DataSource dataSource;
    @Bean
    public PersistentTokenRepository persistentTokenRepository () {
        JdbcTokenRepositoryImpl tokenRepositoryImpl = new JdbcTokenRepositoryImpl();
        tokenRepositoryImpl.setDataSource(dataSource);
        // 启动时自动创建表   如果数据库有该表,再设置为true,启动会报错 所以第一次运行时开启 以后关闭
//        tokenRepositoryImpl.setCreateTableOnStartup(true);
        return tokenRepositoryImpl;
    }

}

认证流程类UserDetailsService接口实现类

package com.wg.securitydemo.service;

import org.springframework.beans.factory.annotation.Autowired;
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.User;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;

import java.util.ArrayList;
import java.util.List;

@Service
public class UserDetailServiceImpl implements UserDetailsService {

    @Autowired
    UserService userService;
    @Autowired
    User_RoleService user_roleService;
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        // 密码是根据username从数据库查询
        String password = userService.GetpassByName(username);
        // 角色权限是数据库权限表查询的,只保留权限那一字段的List<String>万一有多个呢
        List<String> roleList = user_roleService.GetRoleByName(username);
        List<GrantedAuthority> grantedAuthorityList = new ArrayList<>() ;
        /*
         * Spring Boot 2.0 版本踩坑
         * 必须要 ROLE_ 前缀, 因为 hasRole("LEVEL1")判断时会自动加上ROLE_前缀变成 ROLE_LEVEL1 ,
         * 如果不加前缀一般就会出现403错误
         * 在给用户赋权限时,数据库存储必须是完整的权限标识ROLE_LEVEL1
         */
        if (roleList != null && roleList.size()>0){
            for (String role : roleList){
                grantedAuthorityList.add(new SimpleGrantedAuthority(role)) ;
            }
        }
        return new User(username,password,grantedAuthorityList);
    }
}

对密码加密的处理(这里选择不处理)

package com.wg.securitydemo.service;

import org.springframework.security.crypto.password.PasswordEncoder;
//这里不对密码做任何处理简单Demo
public class CustomPasswordEncoder implements PasswordEncoder {
    @Override
    public String encode(CharSequence charSequence) {
        return charSequence.toString();
    }

    @Override
    public boolean matches(CharSequence charSequence, String s) {
        return s.equals(charSequence.toString());
    }
}

页面控制类

package com.wg.securitydemo.controller;

import org.springframework.security.authentication.AnonymousAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller
public class PageController {
    @RequestMapping(value = "/")
    public String Home(Model model){
        //返回当前登录的用户信息,前面说了,他是存在SecurityContextHolder 的全局变量中,所以我们可以这样获取
        Authentication auth = SecurityContextHolder.getContext().getAuthentication();
        if (!(auth instanceof AnonymousAuthenticationToken)) {//匿名用户 判断当前Authentication对象是否为一个AnonymousAuthenticationToken instanceof 严格来说是Java中的一个双目运算符,用来测试一个对象是否为一个类的实例
            Object thisuser = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
            model.addAttribute("my",thisuser);
        }
        return "index";
    }
    @RequestMapping(value = "/login")
    public String Login(){
        return "login";
    }
    @RequestMapping(value = "/logout")
    public String Logout(){return "logout";}
    @RequestMapping(value = "/403")
    public String Error(){
        return "403";
    }

    @RequestMapping(value = "page1/wg1")
    public String GoToWg1(){
        return "page1/wg1";
    }

    @RequestMapping(value = "page2/wg2")
    public String GoToWg2(){
        return "page2/wg2";
    }

    @RequestMapping(value = "page3/wg3")
    public String GoToWg3(){
        return "page3/wg3";
    }

}

其余的代码我就直接按名字贴上来

- User
package com.wg.securitydemo.model;

import javax.persistence.*;

@Entity
@Table(name = "user")
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long uid;

    private String username;
    private String userpass;

    public User() {
    }

    public Long getUid() {
        return uid;
    }

    public void setUid(Long uid) {
        this.uid = uid;
    }

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public String getUserpass() {
        return userpass;
    }

    public void setUserpass(String userpass) {
        this.userpass = userpass;
    }

    @Override
    public String toString() {
        return "User{" +
                "uid=" + uid +
                ", username='" + username + '\'' +
                ", userpass='" + userpass + '\'' +
                '}';
    }
}
/********************************我是分割线***********************************/
- User_Role
package com.wg.securitydemo.model;

import javax.persistence.*;

@Entity
@Table(name = "user_role")
public class User_Role {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long rid;
    private Long uid;
    private String username;
    private String userrole;

    public User_Role() {
    }

    public Long getRid() {
        return rid;
    }

    public void setRid(Long rid) {
        this.rid = rid;
    }

    public Long getUid() {
        return uid;
    }

    public void setUid(Long uid) {
        this.uid = uid;
    }

    public String getUsername() {
        return username;
    }

    public void setUser_name(String user_name) {
        this.username = user_name;
    }

    public String getUser_role() {
        return userrole;
    }

    public void setUser_role(String user_role) {
        this.userrole = user_role;
    }

    @Override
    public String toString() {
        return "User_Role{" +
                "rid=" + rid +
                ", uid=" + uid +
                ", user_name='" + username + '\'' +
                ", user_role='" + userrole + '\'' +
                '}';
    }
}

/********************************我是分割线***********************************/
- UserDao
package com.wg.securitydemo.dao;

import com.wg.securitydemo.model.User;
import org.springframework.data.repository.CrudRepository;
import org.springframework.stereotype.Repository;

import java.util.List;

@Repository
public interface UserDao extends CrudRepository<User,Long> {
    List<User> findByUsername(String name);
}
/********************************我是分割线***********************************/
- User_RoleDao (记住请不要在数据库中用user_name这种名字 带有下划线的)
package com.wg.securitydemo.dao;

import com.wg.securitydemo.model.User_Role;
import org.springframework.data.repository.CrudRepository;
import org.springframework.stereotype.Repository;

import java.util.List;

@Repository
public interface User_RoleDao extends CrudRepository<User_Role,Long> {
    List<User_Role> findByUsername(String name);
}
/********************************我是分割线***********************************/
- UserService
package com.wg.securitydemo.service;

import com.wg.securitydemo.dao.UserDao;
import com.wg.securitydemo.model.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.List;

@Service
public class UserService {
    @Autowired
    UserDao userDao;

    public String GetpassByName(String name){
        List<User> ulist = userDao.findByUsername(name);
        if(ulist!=null&&ulist.size()>0)
            return ulist.get(0).getUserpass();
        else
            return null;
    }
}
/********************************我是分割线***********************************/
- User_RoleService
package com.wg.securitydemo.service;

import com.wg.securitydemo.dao.User_RoleDao;
import com.wg.securitydemo.model.User_Role;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.ArrayList;
import java.util.List;

@Service
public class User_RoleService {
    @Autowired
    User_RoleDao user_roleDao;

    public List<String> GetRoleByName(String name){
        List<User_Role> rlist = user_roleDao.findByUsername(name);
        List<String> slist = new ArrayList<>();
        for(User_Role temp : rlist){
            slist.add(temp.getUser_role());
        }
        return slist;
    }
}
/********************************我是分割线***********************************/
- index.html
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>首页登陆</title>
</head>
<body>
<p th:text="${my}"></p>
<p th:if="${my!=null}" style="text-align: center"><a href="/logout">注销</a></p>
<h3 th:if="${my==null}" style="text-align: center">欢迎来到首页<a href="/login">请登录</a></h3>

<h3 style="text-align: center"><a href="/page1/wg1">去wg1</a>&nbsp;&nbsp;<a href="/page2/wg2">去wg2</a>&nbsp;&nbsp;<a href="/page3/wg3">去wg3</a></h3>
</body>
</html>
/********************************我是分割线***********************************/
- login.html
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>登陆</title>
</head>
<body>
<div align="center">
    <form th:action="@{/login}" method="post">
        用户名:<input name="user"/><br>
        密&nbsp;&nbsp;&nbsp;码:<input name="pwd"><br/>
        <input type="checkbox" name="remeber"> 记住我<br/>
        <input type="submit" value="Login">
    </form>
</div>
</body>
</html>
/********************************我是分割线***********************************/
- logout.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>LogOUT</title>
</head>
<body>
<p style="text-align: center">注销成功</p>
</body>
</html>
/********************************我是分割线***********************************/
- 403.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>403</title>
</head>
<body>
<h1 style="text-align: center">403</h1>
</body>
</html>
/********************************我是分割线***********************************/
wg1.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>WG</title>
</head>
<body>
<P style="text-align: center">欢迎来到page1/wg1</P>
</body>
</html>
/********************************我是分割线***********************************/
- wg2.html wg3.html -.-eMMMMMMM。。。你懂的
- 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>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.1.7.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.wg</groupId>
    <artifactId>securitydemo</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>securitydemo</name>
    <description>Demo project for Spring Boot</description>

    <properties>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jdbc</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid</artifactId>
            <version>1.1.9</version>
        </dependency>
        <dependency>
            <groupId>log4j</groupId>
            <artifactId>log4j</artifactId>
            <version>1.2.17</version>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

运行和记住密码

第一次会自动生成数据库中表
下面这个表用来记住用户的


验证记住密码重启时

文章参考 1 2 3

上一篇下一篇

猜你喜欢

热点阅读