SpringSecurity 集成thymeleaf
当用户没有某权限时,页面不展示该按钮
添加依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.2.2</version>
</dependency>
<dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.thymeleaf.extras</groupId>
<artifactId>thymeleaf-extras-springsecurity5</artifactId>
</dependency>
创建application.yml:
spring:
# 数据源
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://127.0.0.1:3306/security_study?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
username: root
password: 1234
thymeleaf:
cache: false # 开发时可以不使用缓存, 有时候页面改了直接重构, 页面就可以更改, 可以不用重启项目
check-template: true # 是否检查模板(前端thymeleaf页面) , 检查
#mybatis配置
mybatis:
mapper-locations: classpath:mapper/*.xml
type-aliases-package: com.powernode.entity
configuration:
map-underscore-to-camel-case: true
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
新建启动类
package com.powernode;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
@MapperScan("com.powernode.dao")
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
配置实体类
package com.powernode.entity;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class SysUser implements Serializable {
private Integer userId;
private String username;
private String password;
private String sex;
private String address;
private Integer enabled;
private Integer accountNoExpired;
private Integer credentialsNoExpired;
private Integer accountNoLocked;
}
package com.powernode.entity;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class SysMenu implements Serializable {
private Integer id;
private Integer pid;
private Integer type;
private String name;
private String code;
}
配置mapper
package com.powernode.dao;
import com.powernode.entity.SysUser;
import org.apache.ibatis.annotations.Param;
public interface SysUserDao {
/**
* 根据用户名获取用户信息
*
* @param name
* @return
*/
// @Param("userName"): 和参数对应, 形参可以随便命名, 但是注解中的名字是实体类中的属性名,
// 在xml文件中会使用到这个实体类中的属性, 如果不写注解, 就需要两个的名字相同
SysUser getByUserName(@Param("userName") String name);
}
package com.powernode.dao;
import org.apache.ibatis.annotations.Param;
import java.util.List;
public interface SysMenuDao {
List<String> queryPermissionsByUserId(@Param("userId") Integer userId);
}
配置映射文件:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.powernode.dao.SysUserDao">
<select id="getByUserName" resultType="sysUser">
select user_id,
username,
password,
sex,
address,
enabled,
account_no_expired,
credentials_no_expired,
account_no_locked
from sys_user
where username = #{userName}
</select>
</mapper>
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.powernode.dao.SysMenuDao">
<select id="queryPermissionsByUserId" resultType="string">
select distinct sm.code
from sys_role_user sru
inner join sys_role_menu srm on sru.rid = srm.rid
inner join sys_menu sm on srm.mid = sm.id
where sru.uid = #{userId}
</select>
</mapper>
新建安全用户类(实现UserDetails 接口):
package com.powernode.vo;
import com.powernode.entity.SysUser;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
public class MySecurityUser implements UserDetails {
private final SysUser sysUser;
//用于存储权限的list
private List<SimpleGrantedAuthority> authorityList;
public MySecurityUser(SysUser sysUser){
this.sysUser=sysUser;
}
/**
* 返回用户所拥有的权限
* @return
*/
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return authorityList;
}
public void setAuthorityList(List<SimpleGrantedAuthority> authorityList) {
this.authorityList = authorityList;
}
@Override
public String getPassword() {
String myPassword=sysUser.getPassword();
sysUser.setPassword(null); //擦除我们的密码,防止传到前端
return myPassword;
}
@Override
public String getUsername() {
return this.sysUser.getUsername();
}
@Override
public boolean isAccountNonExpired() {
return sysUser.getAccountNoExpired().equals(1);
}
@Override
public boolean isAccountNonLocked() {
return sysUser.getAccountNoLocked().equals(1);
}
@Override
public boolean isCredentialsNonExpired() {
return sysUser.getCredentialsNoExpired().equals(1);
}
@Override
public boolean isEnabled() {
return sysUser.getEnabled().equals(1);
}
}
新建service,根据用户名获取用户信息:
package com.powernode.service;
import com.powernode.entity.SysUser;
import org.apache.ibatis.annotations.Param;
public interface SysUserService {
/**
* 根据用户名获取用户信息
* @param name
* @return
*/
SysUser getByUserName(String name);
}
package com.powernode.service;
import org.apache.ibatis.annotations.Param;
import java.util.List;
public interface SysMenuService {
List<String> queryPermissionsByUserId(Integer userId);
}
新建serviceImpl实现service接口:
package com.powernode.service.impl;
import com.powernode.dao.SysUserDao;
import com.powernode.entity.SysUser;
import com.powernode.service.SysUserService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
@Service
@Slf4j
public class SysUserServiceImpl implements SysUserService {
@Resource
private SysUserDao sysUserDao;
@Override
public SysUser getByUserName(String userName) {
return sysUserDao.getByUserName(userName);
}
}
package com.powernode.service.impl;
import com.powernode.dao.SysMenuDao;
import com.powernode.service.SysMenuService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.util.List;
@Service
@Slf4j
public class SysMenuServiceImpl implements SysMenuService {
@Resource
private SysMenuDao sysMenuDao;
@Override
public List<String> queryPermissionsByUserId(Integer userId) {
return sysMenuDao.queryPermissionsByUserId(userId);
}
}
新建SecurityUserDetailsServiceImpl 实现UserDetailService接口:
package com.powernode.service.impl;
import com.powernode.entity.SysUser;
import com.powernode.service.SysMenuService;
import com.powernode.service.SysUserService;
import com.powernode.vo.MySecurityUser;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
@Service
@Slf4j
public class SecurityUserDetailsServiceImpl implements UserDetailsService {
@Resource
private SysUserService sysUserService;
@Resource
private SysMenuService sysMenuService;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
SysUser sysUser = sysUserService.getByUserName(username);
if (null == sysUser) {
throw new UsernameNotFoundException("该用户不存在");
}
// 根据用户id获取该用户所拥有的权限,List<SimpleGrantedAuthority>
List<String> userPermissions = sysMenuService.queryPermissionsByUserId(sysUser.getUserId());
// 遍历权限,把权限放到列表中
List<SimpleGrantedAuthority> authorityList = new ArrayList<>();
for (String userPermission : userPermissions) {
SimpleGrantedAuthority simpleGrantedAuthority = new SimpleGrantedAuthority(userPermission);
authorityList.add(simpleGrantedAuthority);
}
List<SimpleGrantedAuthority> authorityList1 = userPermissions.stream()
/*.map(userPermission -> {
SimpleGrantedAuthority simpleGrantedAuthority = new SimpleGrantedAuthority(userPermission);
return simpleGrantedAuthority;
})*/
.map(SimpleGrantedAuthority::new)
.collect(Collectors.toList());
MySecurityUser securityUser = new MySecurityUser(sysUser);
securityUser.setAuthorityList(authorityList);
return securityUser;
}
}
创建测试的Studentcontroller、TeacherController:
package com.powernode.controller;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@Controller
@Slf4j
@RequestMapping("/student")
public class StudentController {
@GetMapping("/query")
@PreAuthorize("hasAuthority('student:query')")
public String queryInfo(){
// /templates/student/query.html
return "student/query";
}
@GetMapping("/add")
@PreAuthorize("hasAuthority('student:add')")
public String addInfo(){
return "student/add";
}
@GetMapping("/update")
@PreAuthorize("hasAuthority('student:update')")
public String updateInfo(){
return "student/update";
}
@GetMapping("/delete")
@PreAuthorize("hasAuthority('student:delete')")
public String deleteInfo(){
return "student/delete";
}
@GetMapping("/export")
@PreAuthorize("hasAuthority('student:export')")
public String exportInfo(){
return "student/export";
}
}
package com.powernode.controller;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@Slf4j
@RequestMapping("/teacher")
public class TeacherController {
@GetMapping("/query")
@PreAuthorize("hasAuthority('teacher:query')") //预授权
public String queryInfo(){
return "I am a teacher!";
}
}
新建LoginController和IndexController:
package com.powernode.controller;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
@Controller // 这里不返回json字符串, 返回一个thymeleaf模板
@Slf4j
public class LoginController {
@RequestMapping("/toLogin")
public String toLogin() {
// 返回thymeleaf的逻辑视图名,物理视图=前缀+逻辑视图名+后缀
// /templates/ + login + .html(默认前缀 加 逻辑视图 加 默认的后缀)
// 返回thymeleaf的逻辑视图
return "login";
}
}
package com.powernode.controller;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
@Controller
@Slf4j
@RequestMapping("/index")
public class IndexController {
@RequestMapping("/toIndex")
public String toIndex(){
return "main";
}
}
创建安全配置文件WebSecurityConfig:
package com.powernode.config;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
@EnableGlobalMethodSecurity(prePostEnabled = true)
@Slf4j
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
// 所有请求,都需要认证
http.authorizeRequests().anyRequest().authenticated();
// 这个是排除了的, 不需要登录就可以访问的
http.formLogin()
.loginPage("/toLogin") // 配置登录页面
.usernameParameter("uname") // 用户名参数
.passwordParameter("pwd") // 密码参数
.loginProcessingUrl("/login/doLogin") // 单击登录后进入url
.failureForwardUrl("/toLogin") // 登录失败
.successForwardUrl("/index/toIndex") // 登录成功
.permitAll(); // 配置登录
http.logout().logoutSuccessUrl("/toLogin"); // 配置退出成功登录页面
// 跨域请求(要携带token, 防止被攻击, 这里没有token, 取消了保护)
http.csrf().disable(); // 关闭跨域请求保护
}
}
创建静态页面login.html和main.html:
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" lang="en">
<head>
<meta charset="UTF-8">
<title>用户登陆</title>
</head>
<body>
<h2>登录页面</h2>
<form action="/login/doLogin" method="post">
<table>
<tr>
<td>用户名:</td>
<td><input type="text" name="uname" value="thomas"></td>
</tr>
<tr>
<td>密码:</td>
<td><input type="password" name="pwd"></td>
<span th:if="${param.error}">用户名或者密码错误</span>
</tr>
<tr>
<td colspan="2">
<button type="submit">登录</button>
</td>
</tr>
</table>
</form>
</body>
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/extras/spring-security">
<head>
<meta charset="UTF-8">
<title>系统首页</title>
</head>
<body>
<h1 align="center">系统首页</h1>
<a href="/student/query" sec:authorize="hasAuthority('student:query')">查询学生</a>
<br>
<a href="/student/add" sec:authorize="hasAuthority('student:add')">添加学生</a>
<br>
<a href="/student/update" sec:authorize="hasAuthority('student:update')">更新学生</a>
<br>
<a href="/student/delete" sec:authorize="hasAuthority('student:delete')">删除学生</a>
<br>
<a href="/student/export" sec:authorize="hasAuthority('student:export')">导出学生</a>
<br>
<br><br><br>
<h2><a href="/logout">退出</a></h2>
<br>
</body>
</html>
在templates/student下面创建学生管理的各个页面:
创建export.html
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org"
>
<head>
<meta charset="UTF-8">
<title>系统首页-学生管理</title>
</head>
<body>
<h1 align="center">系统首页-学生管理-导出</h1>
<a href="/index/toIndex">返回</a>
<br>
</body>
</html>
创建query.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>系统首页-学生管理</title>
</head>
<body>
<h1 align="center">系统首页-学生管理-查询</h1>
<a href="/index/toIndex">返回</a>
<br>
</body>
</html>
创建add.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>系统首页-学生管理</title>
</head>
<body>
<h1 align="center">系统首页-学生管理-新增</h1>
<a href="/index/toIndex">返回</a>
<br>
</body>
</html>
创建update.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>系统首页-学生管理</title>
</head>
<body>
<h1 align="center">系统首页-学生管理-更新</h1>
<a href="/index/toIndex">返回</a>
<br>
</body>
</html>
创建delete.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>系统首页-学生管理</title>
</head>
<body>
<h1 align="center">系统首页-学生管理-删除</h1>
<a href="/index/toIndex">返回</a>
<br>
</body>
</html>
创建403页面:
在static/error下面创建403.html
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>403</title>
</head>
<body>
<h2>403:您没有权限访问此页面</h2>
<a href="/index/toIndex">去首页</a>
</body>
</html>
启动测试:
集成图片验证码(集成thymeleaf基础上添加)
Spring Security是通过过滤器链来完成了,所以它的解决方案是创建一个过滤器放到Security的过滤器链中,在自定义的过滤器中比较验证码
添加依赖(用于生成验证码)
<!--引入hutool:可以生成验证码-->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.3.9</version>
</dependency>
添加一个获取验证码的接口:
package com.powernode.controller;
import cn.hutool.captcha.CaptchaUtil;
import cn.hutool.captcha.CircleCaptcha;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import javax.annotation.Resource;
import javax.imageio.ImageIO;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
@Controller
@Slf4j
public class CaptchaController {
/**
* 获取图片验证码
*/
@GetMapping("/code/image")
public void getCaptchaCode(HttpServletRequest request,HttpServletResponse response) throws IOException {
CircleCaptcha circleCaptcha = CaptchaUtil.createCircleCaptcha(200, 100, 2, 20);
String code = circleCaptcha.getCode();
log.info("生成的图片验证码为:{}",code);
//将验证码存储到session中
request.getSession().setAttribute("CAPTCHA_CODE",code);
//将图片写到响应流里,参数一,图片,参数2:图片格式,参数3:响应流
ImageIO.write(circleCaptcha.getImage(),"JPEG",response.getOutputStream());
}
}
创建验证码过滤器:
package com.powernode.filter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import org.thymeleaf.util.StringUtils;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@Component
@Slf4j
public class ValidateCodeFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
// TODO 判断路径 是否是/login/doLogin
String requestURI = request.getRequestURI();
if(!requestURI.equals("/login/doLogin")){// 不是登录请求,直接放行
doFilter(request,response,filterChain); //直接下一个
return;
}
//校验验证码
validateCode(request,response,filterChain);
}
private void validateCode(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws IOException, ServletException {
//1 从前端获取用户输入的验证码
String enterCode = request.getParameter("code");
//2 从session中获取验证码
String captchaCodeInSession = (String) request.getSession().getAttribute("CAPTCHA_CODE");
request.getSession().removeAttribute("captcha_code_error"); //清除提示信息
if(StringUtils.isEmpty(enterCode)){
request.getSession().setAttribute("captcha_code_error","请输入验证码");
response.sendRedirect("/toLogin");
return;
}
if(StringUtils.isEmpty(captchaCodeInSession)){
request.getSession().setAttribute("captcha_code_error","验证码错误");
response.sendRedirect("/toLogin");
return;
}
//3 判断二者是否相等
if(!enterCode.equalsIgnoreCase(captchaCodeInSession)){
request.getSession().setAttribute("captcha_code_error","验证码输入错误");
response.sendRedirect("/toLogin");
return;
}
request.getSession().removeAttribute("CAPTCHA_CODE"); //删除session中的验证码
//如果程序执行到这里,说明验证码正确,放行
this.doFilter(request,response,filterChain);
}
}
修改WebSecurityConfig(重点):
package com.powernode.config;
import com.powernode.filter.ValidateCodeFilter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import javax.annotation.Resource;
@EnableGlobalMethodSecurity(prePostEnabled = true)
@Slf4j
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Resource
private ValidateCodeFilter validateCodeFilter;
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
//在用户名密码认证过滤器前添加图片验证码过滤器
http.addFilterBefore(validateCodeFilter, UsernamePasswordAuthenticationFilter.class);
//所有请求,都需要认证
http.authorizeRequests()
.mvcMatchers("/code/image")
.permitAll() //放开验证码的请求
.anyRequest().authenticated();
http.formLogin()
.loginPage("/toLogin") //配置登录页面
.usernameParameter("uname") //用户名参数
.passwordParameter("pwd") //密码参数
.loginProcessingUrl("/login/doLogin") //单击登录后进入url
.failureForwardUrl("/toLogin") //登录失败
.successForwardUrl("/index/toIndex") //登录成功
.permitAll(); //配置登录
http.logout().logoutSuccessUrl("/toLogin"); //配置退出成功登录页面
http.csrf().disable(); //关闭跨域请求保护
}
}
修改login.html:
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>用户登陆</title>
</head>
<body>
<h2>登录页面</h2>
<form action="/login/doLogin" method="post">
<table>
<tr>
<td>用户名:</td>
<td><input type="text" name="uname" value="thomas"></td>
</tr>
<tr>
<td>密码:</td>
<td><input type="password" name="pwd"></td>
<span th:if="${param.error}">用户名或者密码错误</span>
</tr>
<tr>
<td>验证码:</td>
<td>
<input type="text" name="code"/>
<img src="/code/image" style="height:33px;cursor:pointer;" onclick="this.src=this.src"/>
<span th:text="${session.captcha_code_error}" style="color: #FF0000;">username</span>
</td>
</tr>
<tr>
<td colspan="2">
<button type="submit">登录</button>
</td>
</tr>
</table>
</form>
</body>