目录
SpringSecurity权限管理系统实战—一、项目简介和开发环境准备
SpringSecurity权限管理系统实战—二、日志、接口文档等实现
SpringSecurity权限管理系统实战—三、主要页面及接口实现
SpringSecurity权限管理系统实战—四、整合SpringSecurity(上)
SpringSecurity权限管理系统实战—五、整合SpringSecurity(下)
SpringSecurity权限管理系统实战—六、SpringSecurity整合jwt
SpringSecurity权限管理系统实战—七、处理一些问题
SpringSecurity权限管理系统实战—八、AOP记录用户、异常日志
SpringSecurity权限管理系统实战—九、数据权限的配置
前言
这几天的时间去弄博客了,这个项目就被搁在一边了。
在之前我是用wordpress来搭的博客,用的阿里云的学生机,就卡的不行,体验极差,也没有发布过多少内容。后来又想着自己写一个博客系统,后台部分已经开发了大半,懒癌犯了,就一直搁置了(图片上的所有能点击的接口都实现了)。现在回过去一看,接口十分混乱,冗余。可能不会再用来作为自己的博客了(随便再写写,做个毕设项目吧)
然后又想着用静态博客,绕来绕去后,最终选用了vuepress来搭建静态博客,部署的时候又顺带着复习了下git的知识(平时idea插件用的搞得我git命令都忘得差不多了)。现在的博客是根据vuepress-theme-roco主题魔改的,给张照片感受下
已经部署到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 应用的安全性包括两部分:
- 用户认证(Authentication)
- 用户授权(Authorization)
简单来说,认证就是登录,授权其实就是权限的鉴别,看用户是否具备相应请求的权限。
二、整合SpringSecurity
在SpringBoot中想要使用SpringSecurity,只要添加SpringSecurity的依赖即可
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
这个依赖在最初给的pom中已经有了,不过给注释了,取消掉就可以,其余什么都不用做,启动项目。
启动完成后,我们访问http://localhost:8080或者其中的任何接口,都会重定向到登录页面。
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页面了
三、自定义登录页
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>
重启项目查看
四、动态获取菜单
目前我们的项目还是根据PeaAdmin的menu.json来获取的菜单。这明显不行,没有权限的用户登录后点来点去,发现什么都用不了,这对用户体验来说非常差。所有要根据用户的id来动态的生成菜单。
首先看一下menu.json的格式。
之后的返回的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时再改)
启动项目,查看效果
这里显示拒绝链接是因为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图形验证码生成工具,可生成的类型有如下几种
首先引入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>
重启项目来看一下
目前只是让验证码在前端绘制了出来,我们如果想要使用,还需要自定义一个过滤器
新建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);
重启项目,这时需要我们输入正确的验证码后才能进行登录
剩下的一些我们下一节再来完成
本系列gitee和github中同步更新