SpringSecurity权限管理系统实战—四、整合SpringSecurity(上)

目录

SpringSecurity权限管理系统实战—一、项目简介和开发环境准备
SpringSecurity权限管理系统实战—二、日志、接口文档等实现
SpringSecurity权限管理系统实战—三、主要页面及接口实现
SpringSecurity权限管理系统实战—四、整合SpringSecurity(上)
SpringSecurity权限管理系统实战—五、整合SpringSecurity(下)
SpringSecurity权限管理系统实战—六、SpringSecurity整合jwt
SpringSecurity权限管理系统实战—七、处理一些问题
SpringSecurity权限管理系统实战—八、AOP 记录用户日志、异常日志
SpringSecurity权限管理系统实战—九、数据权限的配置

前言

这几天的时间去弄博客了,这个项目就被搁在一边了。
在之前我是用wordpress来搭的博客,用的阿里云的学生机,就卡的不行,体验极差,也没有发布过多少内容。后来又想着自己写一个博客系统,后台部分已经开发了大半,懒癌犯了,就一直搁置了(图片上的所有能点击的接口都实现了)。现在回过去一看,接口十分混乱,冗余。可能不会再用来作为自己的博客了(随便再写写,做个毕设项目吧)

![在这里插入图片描述]( https://img-blog.csdnimg.cn/20200720120637442.PNG?x-oss-process=image/watermark ,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0hZRENT,size_16,color_FFFFFF,t_70#pic_center)

然后又想着用静态博客,绕来绕去后,最终选用了vuepress来搭建静态博客,部署的时候又顺带着复习了下git的知识(平时idea插件用的搞得我git命令都忘得差不多了)。现在的博客是根据vuepress-theme-roco主题魔改的,给张照片感受下

![在这里插入图片描述]( https://img-blog.csdnimg.cn/20200720120702214.PNG?x-oss-process=image/watermark ,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0hZRENT,size_16,color_FFFFFF,t_70#pic_center)

已经部署到github pages。可以访问www.codermy.cn查看。 目前还没有备案成功,尚未配置cdn,所以可能会加载有点慢。国内也可以访问 witmy.gitee.io 查看。

一、Spring Security 介绍

Spring Security 是Spring项目之中的一个安全模块,可以非常方便与spring项目集成。自从有了 Spring Boot 之后,Spring Boot 对于 Spring Security 提供了 自动化配置方案,可以零配置使用 Spring Security。

其实Spring Security 最早不叫 Spring Security ,叫 Acegi Security,后来才发展成为Spring的子项目。由于SpringBoot的大火,让Spring系列的技术都得到了非常多的关注度,SpringSecurity同样也沾了一把光。

一般来说,Web 应用的安全性包括两部分:

  1. 用户认证(Authentication)
  2. 用户授权(Authorization)

简单来说,认证就是登录,授权其实就是权限的鉴别,看用户是否具备相应请求的权限。

二、整合SpringSecurity

在SpringBoot中想要使用SpringSecurity,只要添加SpringSecurity的依赖即可

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

这个依赖在最初给的pom中已经有了,不过给注释了,取消掉就可以,其余什么都不用做,启动项目。

启动完成后,我们访问http://localhost:8080或者其中的任何接口,都会重定向到登录页面。

![在这里插入图片描述]( https://img-blog.csdnimg.cn/20200715175246293.PNG?x-oss-process=image/watermark ,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0hZRENT,size_16,color_FFFFFF,t_70)

SpringSecurity默认的用户名是user,密码则在启动项目时会打印在控制台上。

Using generated security password: 21d26148-7f1e-403a-9041-1bc62a034871

21d26148-7f1e-403a-9041-1bc62a034871就是密码,每次启动都会分配不一样的密码。SpringSecurity同样支持自定义密码,只要在application.yml中简单配置一下即可

spring:
	security:
    	user:
      		name: admin
      		password: 123456

输入用户名密码,登录后就能访问index页面了

![在这里插入图片描述]( https://img-blog.csdnimg.cn/2020071517532698.PNG?x-oss-process=image/watermark ,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0hZRENT,size_16,color_FFFFFF,t_70)

三、自定义登录页

SpringSecurity默认的登录页在SpringBoot2.0之后已经做过升级了,以前的更丑,就是一个没有样式的form表单。现在这个虽然好看了不少,但是感觉还是单调了些。

那么我们需要新建一个SpringSecurityConfig类继承WebSecurityConfigurerAdapter

@EnableWebSecurity
public class SpringSecurityConfig extends WebSecurityConfigurerAdapter {

     @Override
    public void configure(WebSecurity web) throws Exception {
        web.ignoring().antMatchers("/PearAdmin/**");//放行静态资源
    }
    /**
     * anyRequest          |   匹配所有请求路径
     * access              |   SpringEl表达式结果为true时可以访问
     * anonymous           |   匿名可以访问
     * denyAll             |   用户不能访问
     * fullyAuthenticated  |   用户完全认证可以访问(非remember-me下自动登录)
     * hasAnyAuthority     |   如果有参数,参数表示权限,则其中任何一个权限可以访问
     * hasAnyRole          |   如果有参数,参数表示角色,则其中任何一个角色可以访问
     * hasAuthority        |   如果有参数,参数表示权限,则其权限可以访问
     * hasIpAddress        |   如果有参数,参数表示IP地址,如果用户IP和参数匹配,则可以访问
     * hasRole             |   如果有参数,参数表示角色,则其角色可以访问
     * permitAll           |   用户可以任意访问
     * rememberMe          |   允许通过remember-me登录的用户访问
     * authenticated       |   用户登录后可访问
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .anyRequest().authenticated()
                .and()
                .formLogin()
                .loginPage("/login.html")//登录页面
                .loginProcessingUrl("/login")//登录接口
                .permitAll()
                .and()
                .csrf().disable();//关闭csrf
    }
}

把login.html移动到static目录下,不要忘记把form表单的action替换成/login

<!DOCTYPE html>
<html  xmlns:th="http://www.thymeleaf.org">
	<head>
		<meta charset="utf-8">
		<title></title>
		<link rel="stylesheet" href="/PearAdmin/admin/css/pearForm.css" />
		<link rel="stylesheet" href="/PearAdmin/component/layui/css/layui.css" />
		<link rel="stylesheet" href="/PearAdmin/admin/css/pearButton.css" />
		<link rel="stylesheet" href="/PearAdmin/assets/login.css" />
	</head>
	<body background="PearAdmin/admin/images/background.svg" >
	    <form class="layui-form" action="/login" method="post">
			<div class="layui-form-item">
				<img class="logo" src="PearAdmin/admin/images/logo.png" />
				<div class="title">M-S-P Admin</div>
				<div class="desc">
					Spring Security 权 限 管 理 系 统 实 战
				</div>
			</div>
            <div class="layui-form-item">
				<input id="username" name="username" placeholder="用户名 : " type="text" hover class="layui-input" />
			</div>
			<div class="layui-form-item">
				<input d="password" name="password" placeholder="密 码 : " type="password"  hover class="layui-input" />
			</div>
			<div class="layui-form-item">
				<input type="checkbox" name="" title="记住密码" lay-skin="primary" checked>
			</div>
            <div class="layui-form-item">
				<button style="background-color: #5FB878!important;" class="pear-btn pear-btn-primary login">
					登 入 
				</button>
			</div>
		</form>
		<script src="/PearAdmin/component/layui/layui.js" charset="utf-8"></script>
		<script>
			layui.use(['form', 'element','jquery'], function() {
				var form = layui.form;
				var element = layui.element;
				var $ = layui.jquery;
				
				$("body").on("click",".login",function(){
					location.href="index"
				})
			})
		</script>
	</body>
</html>

重启项目查看

![在这里插入图片描述]( https://img-blog.csdnimg.cn/20200715175350939.PNG?x-oss-process=image/watermark ,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0hZRENT,size_16,color_FFFFFF,t_70#pic_center)

四、动态获取菜单

目前我们的项目还是根据PeaAdmin的menu.json来获取的菜单。这明显不行,没有权限的用户登录后点来点去,发现什么都用不了,这对用户体验来说非常差。所有要根据用户的id来动态的生成菜单。

首先看一下menu.json的格式。

![在这里插入图片描述]( https://img-blog.csdnimg.cn/20200715175409856.PNG?x-oss-process=image/watermark ,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0hZRENT,size_16,color_FFFFFF,t_70)

之后的返回的json格式也要像这样才能被正确解析。

新建一个MenuIndexDto用于封装数据

@Data
public class MenuIndexDto implements Serializable {
    private Integer id;
    private Integer parentId;
    private String title;
    private String icon;
    private Integer type;
    private String href;
    private List<MenuIndexDto> children;
}

MenuDao中新增通过用户id查询菜单的方法

 	@Select("SELECT DISTINCT sp.id,sp.parent_id,sp.name,sp.icon,sp.url,sp.type  " +
            "FROM my_role_user sru " +
            "INNER JOIN my_role_menu srp ON srp.role_id = sru.role_id " +
            "LEFT JOIN my_menu sp ON srp.menu_id = sp.id " +
            "WHERE " +
            "sru.user_id = #{userId}")
    @Result(property = "title",column = "name")
    @Result(property = "href",column = "url")
    List<MenuIndexDto> listByUserId(@Param("userId")Integer userId);

MenuService

List<MenuIndexDto> getMenu(Integer userId);

MenuServiceImpl

@Override
    public List<MenuIndexDto> getMenu(Integer userId) {
        List<MenuIndexDto> list = menuDao.listByUserId(userId);
        List<MenuIndexDto> result = TreeUtil.parseMenuTree(list);
        return result;
    }

这里我写了一个工具方法,用于转换返回格式。TreeUtil添加如下方法

public static List<MenuIndexDto> parseMenuTree(List<MenuIndexDto> list){
        List<MenuIndexDto> result = new ArrayList<MenuIndexDto>();
        // 1、获取第一级节点
        for (MenuIndexDto menu : list) {
            if(menu.getParentId() == 0) {
                result.add(menu);
            }
        }
        // 2、递归获取子节点
        for (MenuIndexDto parent : result) {
            parent = recursiveTree(parent, list);
        }
        return result;
    }

    public static MenuIndexDto recursiveTree(MenuIndexDto parent, List<MenuIndexDto> list) {
        List<MenuIndexDto>children = new ArrayList<>();
        for (MenuIndexDto menu : list) {
            if (Objects.equals(parent.getId(), menu.getParentId())) {
                children.add(menu);
            }
            parent.setChildren(children);
        }
        return parent;
    }

MenuController添加如下方法

 	@GetMapping(value = "/index")
    @ResponseBody
    @ApiOperation(value = "通过用户id获取菜单")
    public List<MenuIndexDto> getMenu(Integer userId) {
        return menuService.getMenu(userId);
    }

在index.html文件中把菜单数据加载地址 先换成/api/menu/index/?userId=1(这里先写死,之后自定义SpringSecurity的userdetail时再改)

启动项目,查看效果

![在这里插入图片描述]( https://img-blog.csdnimg.cn/20200715175439741.PNG?x-oss-process=image/watermark ,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0hZRENT,size_16,color_FFFFFF,t_70)

这里显示拒绝链接是因为SpringSecurity默认拒绝frame中访问。这里我们可以写一个SuccessHandler设置Header,或者在SpringSecurityConfig重写的configure方法中添加如下配置

http.headers().frameOptions().sameOrigin();

再重启项目,就可以正常访问了。

五、改写菜单路由

之前菜单的路由我们是写再HelloController中的,现在我们规定下格式。新建AdminController

@Controller
@RequestMapping("/api")
@Api(tags = "系统:菜单路由")
public class AdminController {
    @Autowired
    private MenuService menuService;

    @GetMapping(value = "/index")
    @ResponseBody
    @ApiOperation(value = "通过用户id获取菜单")
    public List<MenuIndexDto> getMenu(Integer userId) {
        return menuService.getMenu(userId);
    }

    @GetMapping("/console")
    public String console(){
        return "console/console1";
    }

    @GetMapping("/403")
    public String error403(){
        return "error/403";
    }

    @GetMapping("/404")
    public String error404(){
        return "error/404";
    }

    @GetMapping("/500")
    public String error500(){
        return "error/500";
    }
    @GetMapping("/admin")
    public String admin(){
        return "index";
    }
}

再去相应页面改写下路由就可以

六、图形验证码

验证码主要是防止机器大规模注册,机器暴力破解数据密码等危害。

EasyCaptcha是一个Java图形验证码生成工具,可生成的类型有如下几种
![在这里插入图片描述]( https://img-blog.csdnimg.cn/20200720120740799.PNG?x-oss-process=image/watermark ,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0hZRENT,size_16,color_FFFFFF,t_70#pic_center)

首先引入maven

<dependencies>
   <dependency>
      <groupId>com.github.whvcse</groupId>
      <artifactId>easy-captcha</artifactId>
      <version>1.6.2</version>
   </dependency>
</dependencies>

新建一个CaptchaController

@Controller
public class CaptchaController {
    
    @RequestMapping("/captcha")
    public void captcha(HttpServletRequest request, HttpServletResponse response) throws Exception {
        CaptchaUtil.out(request, response);
    }
}

再login.html 密码所在的div后面添加如下代码(这里我添加了一下css格式,具体不贴了,自己操作吧)

<div class="layui-form-item">
                <input id="captcha" name="captcha" placeholder="验 证 码:" type="text"  hover class="layui-verify" style="border: 1px solid #dcdfe6;">
                <img src="/captcha" width="130px" height="44px" onclick="this.src=this.src+'?'+Math.random()" title="点击刷新"/>
</div>

重启项目来看一下

![在这里插入图片描述]( https://img-blog.csdnimg.cn/20200720120752866.PNG?x-oss-process=image/watermark ,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0hZRENT,size_16,color_FFFFFF,t_70#pic_center)

目前只是让验证码在前端绘制了出来,我们如果想要使用,还需要自定义一个过滤器

新建VerifyCodeFilter继承OncePerRequestFilter

@Component
public class VerifyCodeFilter extends OncePerRequestFilter {
    private String defaultFilterProcessUrl = "/login";
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException {
        if ("POST".equalsIgnoreCase(request.getMethod()) && defaultFilterProcessUrl.equals(request.getServletPath())) {
            // 登录请求校验验证码,非登录请求不用校验
            HttpSession session = request.getSession();
            String requestCaptcha = request.getParameter("captcha");
            String genCaptcha = (String) request.getSession().getAttribute("captcha");//验证码的信息存放在seesion种,具体看EasyCaptcha官方解释
            if (StringUtils.isEmpty(requestCaptcha)){
                session.removeAttribute("captcha");//删除缓存里的验证码信息
                throw new AuthenticationServiceException("验证码不能为空!");
            }
            if (!genCaptcha.toLowerCase().equals(requestCaptcha.toLowerCase())) {
                session.removeAttribute("captcha");
                throw new AuthenticationServiceException("验证码错误!");
            }
        }
        chain.doFilter(request, response);
    }
}

最后在SpringSecurity种配置该过滤器

@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SpringSecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    private UserDetailsService userDetailsService;
    @Autowired
    private VerifyCodeFilter verifyCodeFilter;

    @Override
    public void configure(WebSecurity web) throws Exception {
        web.ignoring().antMatchers("/PearAdmin/**");//放行静态资源
    }
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.headers().frameOptions().sameOrigin();
        http.addFilterBefore(verifyCodeFilter, UsernamePasswordAuthenticationFilter.class);
        http.authorizeRequests()
                .antMatchers("/captcha").permitAll()//任何人都能访问这个请求
                .anyRequest().authenticated()
                .and()
            .formLogin()
                .loginPage("/login.html")//登录页面 不设限访问
                .loginProcessingUrl("/login")//拦截的请求
                .successForwardUrl("/api/admin")
                .permitAll()
                .and()
            .csrf().disable();//关闭csrf
    }
}

http.addFilterBefore(verifyCodeFilter, UsernamePasswordAuthenticationFilter.class);

重启项目,这时需要我们输入正确的验证码后才能进行登录
剩下的一些我们下一节再来完成
在这里插入图片描述
本系列giteegithub中同步更新

posted @ 2020-08-18 15:08  codermy  阅读(3454)  评论(0)    收藏  举报