SpringBoot整合SpringSecurity(附带源码)

配置环境

配置idea

我使用的是idea,点击New Project
在这里插入图片描述点击next项目信息配置随意,再下一步选上下图所示的组件
在这里插入图片描述

配置thymeleaf

在pom.xml中加入

<dependency>
            <groupId>net.sourceforge.nekohtml</groupId>
            <artifactId>nekohtml</artifactId>
            <version>1.9.22</version>
 </dependency>

在application.yml中加入

spring:
	thymeleaf:
    	suffix: .html
    	cache: false
    	//设置为传统模式,防止因为严格的语法检测遇到的各种麻烦,例如<html />后习惯不会去加斜杠就会被当做错误检测
    	mode: LEGACYHTML5

配置传统检测模式需要额外导入上述的dependency并配合配置文件

配置JPA

在application.yml中加入

spring:

  datasource:
    url: jdbc:mysql://localhost:3306/springsecurity
    driver-class-name: com.mysql.jdbc.Driver
    username: root
    password: root

  jpa:
    show-sql: true
    //由于jpa默认将驼峰命名的entity转化为带下划线的名称去匹配数据库中的表名,而我在数据库中也是使用驼峰命名,所以需要下入下列的配置
    hibernate:
      naming:
        physical-strategy: org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl
    

配置数据库

导入如下代码


DROP TABLE IF EXISTS `Sys_permission`;

CREATE TABLE `Sys_permission` (
  `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
  `name` varchar(200) DEFAULT NULL,
  `description` varchar(200) DEFAULT NULL,
  `url` varchar(200) DEFAULT NULL,
  `pid` int(11) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

LOCK TABLES `Sys_permission` WRITE;
/*!40000 ALTER TABLE `Sys_permission` DISABLE KEYS */;

INSERT INTO `Sys_permission` (`id`, `name`, `description`, `url`, `pid`)
VALUES
	(1,'ROLE_HOME','index','/',NULL),
	(2,'ROLE_ADMIN','admin','/admin',NULL),
	(3,'ROLE_USER','user','/user',NULL);

/*!40000 ALTER TABLE `Sys_permission` ENABLE KEYS */;
UNLOCK TABLES;


# Dump of table Sys_permission_role
# ------------------------------------------------------------

DROP TABLE IF EXISTS `Sys_permission_role`;

CREATE TABLE `Sys_permission_role` (
  `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
  `role_id` int(11) unsigned NOT NULL,
  `permission_id` int(11) unsigned NOT NULL,
  PRIMARY KEY (`id`),
  KEY `role2` (`role_id`),
  KEY `permission` (`permission_id`),
  CONSTRAINT `permission` FOREIGN KEY (`permission_id`) REFERENCES `Sys_permission` (`id`),
  CONSTRAINT `role2` FOREIGN KEY (`role_id`) REFERENCES `Sys_Role` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

LOCK TABLES `Sys_permission_role` WRITE;
/*!40000 ALTER TABLE `Sys_permission_role` DISABLE KEYS */;

INSERT INTO `Sys_permission_role` (`id`, `role_id`, `permission_id`)
VALUES
	(10,2,1),
	(11,2,3),
	(12,3,1),
	(13,3,2),
	(15,2,2);

/*!40000 ALTER TABLE `Sys_permission_role` ENABLE KEYS */;
UNLOCK TABLES;


# Dump of table Sys_Role
# ------------------------------------------------------------

DROP TABLE IF EXISTS `Sys_Role`;

CREATE TABLE `Sys_Role` (
  `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
  `name` varchar(200) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

LOCK TABLES `Sys_Role` WRITE;
/*!40000 ALTER TABLE `Sys_Role` DISABLE KEYS */;

INSERT INTO `Sys_Role` (`id`, `name`)
VALUES
	(2,'ROLE_USER'),
	(3,'ROLE_ADMIN');

/*!40000 ALTER TABLE `Sys_Role` ENABLE KEYS */;
UNLOCK TABLES;


# Dump of table Sys_Role_User
# ------------------------------------------------------------

DROP TABLE IF EXISTS `Sys_Role_User`;

CREATE TABLE `Sys_Role_User` (
  `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
  `Sys_User_id` int(11) unsigned NOT NULL,
  `Sys_Role_id` int(11) unsigned NOT NULL,
  PRIMARY KEY (`id`),
  KEY `user` (`Sys_User_id`),
  KEY `role` (`Sys_Role_id`),
  CONSTRAINT `role` FOREIGN KEY (`Sys_Role_id`) REFERENCES `Sys_Role` (`id`),
  CONSTRAINT `user` FOREIGN KEY (`Sys_User_id`) REFERENCES `Sys_User` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

LOCK TABLES `Sys_Role_User` WRITE;
/*!40000 ALTER TABLE `Sys_Role_User` DISABLE KEYS */;

INSERT INTO `Sys_Role_User` (`id`, `Sys_User_id`, `Sys_Role_id`)
VALUES
	(6,1,3),
	(7,2,2);

/*!40000 ALTER TABLE `Sys_Role_User` ENABLE KEYS */;
UNLOCK TABLES;


# Dump of table Sys_User
# ------------------------------------------------------------

DROP TABLE IF EXISTS `Sys_User`;

CREATE TABLE `Sys_User` (
  `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
  `username` varchar(200) DEFAULT NULL,
  `password` varchar(200) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

LOCK TABLES `Sys_User` WRITE;
/*!40000 ALTER TABLE `Sys_User` DISABLE KEYS */;

INSERT INTO `Sys_User` (`id`, `username`, `password`)
VALUES
	(1,'admin','6d789d4353c72e4f625d21c6b7ac2982'),
	(2,'user','36f1cab655c5252fc4f163a1409500b8');

/*!40000 ALTER TABLE `Sys_User` ENABLE KEYS */;
UNLOCK TABLES;

最后会生成5个表,分别是用户表,角色表,权限表,用户角色中间表,角色权限中间表。

配置包目录

在这里插入图片描述
其中entity可以用idea进行生成,
在这里插入图片描述
在这里插入图片描述

配置dao层

需要新建UserDao和PermissionDao两个类

/**
* UserDao
*/
@Repository
public interface UserDao extends JpaRepository<User,Integer> {
    //自定义一个根据姓名查找用户的方法
    public User findByUsername(String userName);
}


/**
* PermissionDao
* 无需自定义方法,直接使用jpa封装好的就可以
*/
public interface PermissionDao extends JpaRepository<Permission,Integer> {
}

配置Entity关系

在本demo中需要配置两个关系,分别是用户与角色的多对多关系,角色和权限的多对多关系。
在User类中加入:

private List<Role> roles = new ArrayList<>();
/**
     * @ManyToMany 表示多对多关系,fetch = FetchType.EAGER配置懒加载策略为立即加载,因为多对多涉及到树形结构的第二层,
     * 使用懒加载会在使用roles对象时才去数据库查询,但是在本项目中会出现no session,暂时无法解决,所以加上次配置
     *
     * @JoinTable  name:中间表名, @joinColumn : name:在中间表中对应外键名,referencedColumnName在原先表中的主键名
     *
     * inverseJoinColumns中的@joinColumn : name:多的另一方在中间表中对应的主键名,referencedColumnName在原先表中的主键名
     *
     * 此处的配置表明user和role的多对多关系由user维护
     */
    @ManyToMany(fetch = FetchType.EAGER)
    @JoinTable(name = "Sys_Role_User", joinColumns = {@JoinColumn(name = "Sys_User_id", referencedColumnName = "id")},
            inverseJoinColumns = {@JoinColumn(name = "Sys_Role_id", referencedColumnName = "id")})
    public List<Role> getRoles() {
        return roles;
    }

    public void setRoles(List<Role> roles) {
        this.roles = roles;
    }

在Role中加入:

private List<User> users = new ArrayList<>();
//mappedBy:映射的名字为user中role集合的名字
 @ManyToMany(mappedBy = "roles")
    public List<User> getUsers() {
        return users;
    }

    public void setUsers(List<User> users) {
        this.users = users;
    }

Permission和Role的多对多映射也是如此,就不贴出来了。

简单的环境搭建就到此结束了,一切从简所以没有配置连接池。

SpringSecurity配置

我们先做个简单的尝试,在配置好以上步骤后,在template中加入一个名为index的页面

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>

<h1>登录成功</h1>

<h2><a href="/logout">退出</a></h2>
</body>
</html>

并创建一个LoginController,在controller中设置映射路径为/index。
我们启动项目,在地址栏中输入localhost:8080/index,你会发现自动跳转到了一个登陆界面,我们完全没有写过 登陆界面,所以这个是springsecurity自带的一个登录页,登陆的用户名为user,密码是输出在console中的uuid字符串。
在这里插入图片描述登陆以后就可以访问Index页面了。在我们配置Security之前,它默认拦截所有页面并会自动生成一个登陆的账号密码,但这显然不是我们想要的样子。下面我们对它进行改造。

配置拦截策略

首先我们在security包下创建一个类,名字为WebSecurityConfig,继承WebSecurityConfigurerAdapter

//这两个注解缺一不可
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    //重写参数为HttpSecurity的configure方法,配置拦截策略
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
        		//自定义登陆页面
                .formLogin().loginPage("/login")
                	//登陆成功后跳转的页面
                .defaultSuccessUrl("/index")
                	//登陆失败或无权限跳转页面
                .failureUrl("/login-error")
                .permitAll()
                	//其他所有页面必须验证后才可以访问
                .and().authorizeRequests().anyRequest().authenticated()
                //不加上不验证。不知道为什么
                .and().csrf().disable();

    }

}

引用网上的图片
在这里插入图片描述顺带创建两个HTML

login.html

<form  class="form-signin" action="/login" method="post">
    <h2 class="form-signin-heading">用户登录</h2>
    <table>
        <tr>
            <td>用户名:</td>
            <td><input type="text" name="username"  class="form-control"  placeholder="请输入用户名"/></td>
        </tr>
        <tr>
            <td>密码:</td>
            <td><input type="password" name="password"  class="form-control" placeholder="请输入密码" /></td>
        </tr>
        <tr>

            <td colspan="2">
                <button type="submit"  class="btn btn-lg btn-primary btn-block" >登录</button>
            </td>
        </tr>
    </table>
</form>
error.html

<h1>error</h1>

此时再运行项目,输入localhost:8080/跳转到的页面就是我们刚才写好的页面了,随便输入账号密码点击登录的报错页面也是刚刚添加的error页面。

自定义登陆账号验证

实际开发中我们需要在数据库中存储用户的账号密码信息,所以我们需要自定义验证方式。
在security文件夹中创建MyUserDetialsService类 实现UserDetailsService接口

@Service
public class MyUserDetialsService implements UserDetailsService {

    @Autowired
    UserDao userDao;

    @Override
    public UserDetails loadUserByUsername(String userName) throws UsernameNotFoundException {
        //此处的user是entity包中的user
        com.security.entity.User user = userDao.findByUsername(userName);
        if (user != null) {
            List<GrantedAuthority> grantedAuthorities = new ArrayList<>();
            //获取用户的角色集合
            List<Role> roles = user.getRoles();
            //遍历角色集合,并获取每个角色拥有的权限
            for (Role role : roles) {
                List<Permission> permissions = role.getPermissions();
                for (Permission permission :permissions) {
                    //为每个授权中心对象写入权限名
                    grantedAuthorities.add(new SimpleGrantedAuthority(permission.getName()));
                }
            }
            /**此处的user是springsecurity中的一个实现了UserDetails接口的user类,因为我们没有将entity中的user去实现
             * UserDetails接口,所以只能在此处调用实现好的构造方法
             */
            return new User(user.getUsername(), user.getPassword(), grantedAuthorities);
        }
        return null;
    }
}

此处我们的密码使用MD5配合加密盐进行加密,所以需要在utils包中创建MD5Utils类

public class MD5Util {

    private static final String SALT = "tamboo";

    public static String encode(String password) {
        password = password + SALT;
        MessageDigest md5 = null;
        try {
            md5 = MessageDigest.getInstance("MD5");
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
        char[] charArray = password.toCharArray();
        byte[] byteArray = new byte[charArray.length];

        for (int i = 0; i < charArray.length; i++)
            byteArray[i] = (byte) charArray[i];
        byte[] md5Bytes = md5.digest(byteArray);
        StringBuffer hexValue = new StringBuffer();
        for (int i = 0; i < md5Bytes.length; i++) {
            int val = ((int) md5Bytes[i]) & 0xff;
            if (val < 16) {
                hexValue.append("0");
            }
            hexValue.append(Integer.toHexString(val));
        }
        return hexValue.toString();
    }
}

在先前创建好的WebSecurityConfig中加入如下配置:

//注入我们刚才写好的service类
@Autowired
    MyUserDetialsService userService;

    //配置加密
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userService).passwordEncoder(new PasswordEncoder() {
            //加密
            @Override
            public String encode(CharSequence rawPassword) {
                return MD5Util.encode((String) rawPassword);
            }

            //解密,前者是输入的密码,后者是数据库查询的密码
            @Override
            public boolean matches(CharSequence rawPassword, String encodedPassword) {
                return encodedPassword.equals(MD5Util.encode((String) rawPassword));
            }
        });
    }

实际上我们刚才写的代码中并没有对密码进行验证,SpringSecurity中已经在内部写好了验证代码,我们只需要将查询到的user对象转换为UserDetail对象返回给框架即可。此时再次运行demo登陆的账号密码就可以使用数据库中自定义的了,我目前设置的默认账号密码为admin:admin,user:user。

配置自定义权限验证

在security包下创建MyInvocationSecurityMetadataSourceService实现FilterInvocationSecurityMetadataSource接口。该类用于加载权限表中的url信息,并和request的url进行对比,有匹配则将该URL所需要的权限返回给decide()方法,不存在则返回空

@Service
public class MyInvocationSecurityMetadataSourceService implements FilterInvocationSecurityMetadataSource {

    @Autowired
    private PermissionDao permissionDao;

    private HashMap<String, Collection<ConfigAttribute>> map = null;

    //在demo启动第一个用户登陆后,加载所有权限进map
    public void loadResourceDefine() {
        map = new HashMap<>();
        Collection<ConfigAttribute> array;
        ConfigAttribute cfg;
        List<Permission> permissions = permissionDao.findAll();
        for (Permission permission : permissions) {
            array = new ArrayList<>();
            //此处只添加了用户的名字,其实还可以添加更多权限的信息,例如请求方法到ConfigAttribute的集合中去。此处添加的信息将会作为MyAccessDecisionManager类的decide的第三个参数。
            cfg = new SecurityConfig(permission.getName());
            array.add(cfg);
            //用权限的getUrl() 作为map的key,用ConfigAttribute的集合作为 value
            map.put(permission.getUrl(), array);
        }
    }

    @Override
    public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException {
        if(map ==null) {
            loadResourceDefine();
        }
        HttpServletRequest request = ((FilterInvocation) object).getHttpRequest();
        AntPathRequestMatcher matcher;
        //遍历权限表中的url
        for (String url : map.keySet()) {
            matcher = new AntPathRequestMatcher(url);
            //与request对比,符合则说明权限表中有该请求的URL
            if(matcher.matches(request)) {
                return map.get(url);
            }
        }
        return null;
    }

    @Override
    public Collection<ConfigAttribute> getAllConfigAttributes() {
        return null;
    }

    @Override
    public boolean supports(Class<?> aClass) {
        return true;
    }
}

接着在security下新建MyAccessDecisionManager类实现AccessDecisionManager接口。该类为决策类,决策该用户的request是否有权限访问。

@Service
public class MyAccessDecisionManager implements AccessDecisionManager {

    /**
     * @param authentication UserService中循环添加到GrantedAuthority中的权限信息集合
     * @param object         包含客户端发起的请求的request信息,可以转换为HTTPRequest
     * @param collection     url所需的权限集合
     * @throws AccessDeniedException
     * @throws InsufficientAuthenticationException
     */
    @Override
    public void decide(Authentication authentication, Object object, Collection<ConfigAttribute> collection) throws AccessDeniedException, InsufficientAuthenticationException {
        //判断URL所需的权限集合是否为空,为空则放行
        if (null == collection || collection.size() <= 0) {
            return;
        }
        String needPermission;
        for (ConfigAttribute c : collection) {
            //获得所需的权限
            needPermission = c.getAttribute();
            //遍历用户拥有的权限与URL所需的权限进行对比
            for (GrantedAuthority ga : authentication.getAuthorities()) {
                if (needPermission.trim().equals(ga.getAuthority())){
                    return;
                }
            }
        }
        throw new AccessDeniedException("no permission");
    }

    @Override
    public boolean supports(ConfigAttribute configAttribute) {
        return true;
    }

    @Override
    public boolean supports(Class<?> aClass) {
        return true;
    }
}

最后在security包下创建MyFilterSecurityInterceptor类

@Service
public class MyFilterSecurityInterceptor extends AbstractSecurityInterceptor implements Filter {


    @Autowired
    private FilterInvocationSecurityMetadataSource securityMetadataSource;

    //设置决策器
    @Autowired
    public void setMyAccessDecisionManager(MyAccessDecisionManager myAccessDecisionManager) {
        super.setAccessDecisionManager(myAccessDecisionManager);
    }


    @Override
    public void init(FilterConfig filterConfig) throws ServletException {

    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {

        FilterInvocation fi = new FilterInvocation(request, response, chain);
        invoke(fi);
    }


    public void invoke(FilterInvocation fi) throws IOException, ServletException {
//fi里面有一个被拦截的url
//里面调用MyInvocationSecurityMetadataSource的getAttributes(Object object)这个方法获取fi对应的所有权限
//再调用MyAccessDecisionManager的decide方法来校验用户的权限是否足够
        InterceptorStatusToken token = super.beforeInvocation(fi);
        try {
//执行下一个拦截器
            fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
        } finally {
            super.afterInvocation(token, null);
        }
    }


    @Override
    public void destroy() {

    }

    @Override
    public Class<?> getSecureObjectClass() {
        return FilterInvocation.class;

    }

    //添加判断url所需的权限类
    @Override
    public SecurityMetadataSource obtainSecurityMetadataSource() {
        return this.securityMetadataSource;
    }
}

创建

admin.html
<p class="bg-info">Admin权限访问</p>
index2.html
<p>这是没录入数据库的url</p>
user.html
<p>User权限访问</p>

将这些页面加入controller映射。
数据库中设置的关系为:

用户 角色 权限
admin ROLE_USER,ROLE_ADMIN ROLE_ADMIN,ROLE_HOME,ROLE_User
user ROLE_USER ROLE_HOME,ROLE_User

在这里插入图片描述权限表中映射的URL如图。
运行demo
结果如下:
登陆admin账号,所有页面都可以访问
登陆user账号,除了/admin无权限访问,其他都可以访问。
/index2没有录入数据库,但是在任何用户登陆以后都可以访问。

源码地址:

https://gitee.com/king176/springbootdemo_source_code/tree/master

猜你喜欢

转载自blog.csdn.net/weixin_43184769/article/details/84937685