1.认证功能
1.1默认用户登录
依赖导入
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</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>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
使用后登录显示security的登陆页面
1.2自定义用户名与密码
yml中设置
spring:
application:
name: security-book
security:
user:
name: admin
password: 123456
或者在WebSecurityConfig中配置
package com.example.securitybook.config;
import com.example.securitybook.filter.ValidCodeFilter;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.configurers.provisioning.InMemoryUserDetailsManagerConfigurer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import java.io.IOException;
import java.io.PrintWriter;
@Configuration
public class WebSecurityConfig {
@Autowired
private ValidCodeFilter validCodeFilter;
@Bean
PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
//如果不想加密就返回
//return NoOpPasswordEncoder.getInstance();
}
@Bean
public UserDetailsService userDetailsService(){
//使用内存数据进行认证
InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
//创建4个用户
UserDetails user1 = User.withUsername("admin").password(passwordEncoder().encode("123"))
.roles("role1","role2","role3").build();
UserDetails user2 = User.withUsername("user2").password(passwordEncoder().encode("123"))
.roles("role1").build();
UserDetails user3 = User.withUsername("user3").password(passwordEncoder().encode("123"))
.roles("role2").build();
UserDetails user4 = User.withUsername("user4").password(passwordEncoder().encode("123"))
.roles("role3").build();
manager.createUser(user1);
manager.createUser(user2);
manager.createUser(user3);
manager.createUser(user4);
return manager;
}
}
1.3访问控制
1.同样在WebSecurityConfig中配置
@Bean
public WebSecurityCustomizer webSecurityCustomizer(){
//忽略这些静态资源
return(web -> web.ignoring().requestMatchers("/js/**","/css/**","/images/**"));
}
2.WebSecurityConfig中添加filterChain方法,开启登录配置
@Bean
SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception{
//开启登陆配置
httpSecurity.authorizeHttpRequests()
//允许直接访问
.requestMatchers("/","/index","/validcode")
.permitAll()
//其他任何请求都必须经过身份验证
.anyRequest()
.authenticated();
//开启表单验证
httpSecurity.formLogin();
return httpSecurity.build();
}
1.4自定义登陆界面与注销登录
1.创建登录页面login.html
2.在Controller中添加登录页面的访问路径
@GetMapping("/toLogin")
public String tologin(){
return "login";
}
3.在WebSecurityConfig中配置
httpSecurity.formLogin().loginPage("/toLogin")//跳转到自定义的登录页面
.usernameParameter("username")//自定义表单的用户名的name,默认为username
.passwordParameter("password")//自定义表单的密码的name,默认为password
.loginProcessingUrl("/login")//表单请求的地址,使用Security定义好的/login,并且与自定义表单的action一致
.permitAll();//允许访问登录有关的路径
4.实现注销,在WebSecurityConfig中配置
//开启注销
httpSecurity.logout().logoutSuccessUrl("/index");
//关闭csrf
httpSecurity.csrf().disable();
完整配置
package com.example.securitybook.config;
import com.example.securitybook.filter.ValidCodeFilter;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.configurers.provisioning.InMemoryUserDetailsManagerConfigurer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import java.io.IOException;
import java.io.PrintWriter;
@Configuration
public class WebSecurityConfig {
@Autowired
private ValidCodeFilter validCodeFilter;
@Bean
PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
//如果不想加密就返回
//return NoOpPasswordEncoder.getInstance();
}
@Bean
public UserDetailsService userDetailsService(){
//使用内存数据进行认证
InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
//创建4个用户
UserDetails user1 = User.withUsername("admin").password(passwordEncoder().encode("123"))
.roles("role1","role2","role3").build();
UserDetails user2 = User.withUsername("user2").password(passwordEncoder().encode("123"))
.roles("role1").build();
UserDetails user3 = User.withUsername("user3").password(passwordEncoder().encode("123"))
.roles("role2").build();
UserDetails user4 = User.withUsername("user4").password(passwordEncoder().encode("123"))
.roles("role3").build();
manager.createUser(user1);
manager.createUser(user2);
manager.createUser(user3);
manager.createUser(user4);
return manager;
}
@Bean
public WebSecurityCustomizer webSecurityCustomizer(){
//忽略这些静态资源
return(web -> web.ignoring().requestMatchers("/js/**","/css/**","/images/**"));
}
@Bean
SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception{
httpSecurity.addFilterBefore(validCodeFilter, UsernamePasswordAuthenticationFilter.class);
//开启登陆配置
httpSecurity.authorizeHttpRequests()
//允许直接访问
.requestMatchers("/","/index","/validcode")
.permitAll()
//其他任何请求都必须经过身份验证
.anyRequest()
.authenticated();
//开启表单验证
httpSecurity.formLogin().loginPage("/toLogin")//跳转到自定义的登录页面
.usernameParameter("username")//自定义表单的用户名的name,默认为username
.passwordParameter("password")//自定义表单的密码的name,默认为password
.loginProcessingUrl("/login")//表单请求的地址,使用Security定义好的/login,并且与自定义表单的action一致
//.failureUrl("/toLogin/error")//如果登录失败跳转到
.permitAll()//允许访问登录有关的路径
.failureHandler(new AuthenticationFailureHandler() {
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
response.setContentType("application/json;charset = utf-8");
PrintWriter out = response.getWriter();
String json = "{\"status\":\"error\",\"msg\":\""+exception.getMessage()+"\"}";
out.write(json);
}
});
//开启注销
httpSecurity.logout().logoutSuccessUrl("/index");
//关闭csrf
httpSecurity.csrf().disable();
//记住我
httpSecurity.rememberMe();
return httpSecurity.build();
}
}
1.5登录认证失败处理
1.在表单验证那里添加.failureUrl("/toLogin/error")
//开启表单验证
httpSecurity.formLogin().loginPage("/toLogin")//跳转到自定义的登录页面
.usernameParameter("username")//自定义表单的用户名的name,默认为username
.passwordParameter("password")//自定义表单的密码的name,默认为password
.loginProcessingUrl("/login")//表单请求的地址,使用Security定义好的/login,并且与自定义表单的action一致
.failureUrl("/toLogin/error")//如果登录失败跳转到
.permitAll();//允许访问登录有关的路径
2.在控制器中添加方法,对应上述的URL
@GetMapping("/toLogin/error")
public String toLogin(HttpServletRequest request, Model model){
AuthenticationException authenticationException = (AuthenticationException) request.getSession().getAttribute(WebAttributes.AUTHENTICATION_EXCEPTION);
if (authenticationException instanceof UsernameNotFoundException || authenticationException instanceof BadCredentialsException){
model.addAttribute("msg","用户名或密码错误");
}else if (authenticationException instanceof DisabledException){
model.addAttribute("msg","用户名被禁用");
}else if (authenticationException instanceof LockedException){
model.addAttribute("msg","账户过期");
}else if (authenticationException instanceof CredentialsExpiredException){
model.addAttribute("msg","证书过期");
}
return "login";
}
3.修改前端login.html页面,添加显示错误信息的代码
<span th:text="${msg}" style="color:red"></span><br/>
完整前端代码
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>login</title>
</head>
<body>
<h1>用户登录</h1>
<form th:action="@{/login}" method="post">
用户名:<input type="text" name="username"><br/>
密 码:<input type="password" name="password"><br/>
验证码:<input type="text" name="code"><img src="/validcode" width="100" height="40"/><br/>
记住我:<input type="checkbox" name="remember-me"/><br/>
<input type="submit" value="登录"><br/>
<span th:text="${msg}" style="color:red"></span><br/>
</form>
</body>
</html>
1.6记住用户名
1.在配置类中的filterChain方法下添加代码
//记住我
httpSecurity.rememberMe();
2.修改login.html,添加“记住我”选择框,注意name属性必须是remember-me
记住我:<input type="checkbox" name="remember-me"/><br/>
1.7图形验证码的使用
1.创建工具类ValidCode,用于生成验证码
package com.example.securityBook.util;
import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.io.OutputStream;
import java.util.Random;
public class ValidCode {
private int width = 100;//验证码图片的宽度
private int height = 40;//验证码图片的高度
private String[] fontNames = { "宋体", "楷体", "隶书", "微软雅黑" };//可选择的字体
private Color bgColor = new Color(255, 255, 255);//设置验证码图片的背景颜色为白色
private Random random = new Random();
private String codes = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
private String validcode;// 保存随机数字(即验证码)
private Color getColor() { //随机生成一个颜色
int red = random.nextInt(200);
int green = random.nextInt(200);
int blue = random.nextInt(200);
return new Color(red, green, blue);
}
private Font getFont() {//随机字体
String name = fontNames[random.nextInt(fontNames.length)];
int style = random.nextInt(4);
int size = random.nextInt(5) + 24;
return new Font(name, style, size);
}
private char getChar() { //随机字符
return codes.charAt(random.nextInt(codes.length()));
}
private BufferedImage createImage() { //创建BufferedImage对象
BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
Graphics2D g2 = (Graphics2D) image.getGraphics();
g2.setColor(bgColor);// 设置验证码图片的背景颜色
g2.fillRect(0, 0, width, height);
return image;
}
public BufferedImage getImage() {
BufferedImage image = createImage();
Graphics2D g2 = (Graphics2D) image.getGraphics();
StringBuffer sb = new StringBuffer();
for (int i = 0; i < 4; i++) {
String s = getChar() + "";
sb.append(s);
g2.setColor(getColor());
g2.setFont(getFont());
float x = i * width * 1.0f / 4;
g2.drawString(s, x, height - 15);
}
this.validcode = sb.toString();
drawLine(image);
return image;
}
private void drawLine(BufferedImage image) {//绘制干扰线
Graphics2D g2 = (Graphics2D) image.getGraphics();
int num = 6;
for (int i = 0; i < num; i++) {
int x1 = random.nextInt(width);
int y1 = random.nextInt(height);
int x2 = random.nextInt(width);
int y2 = random.nextInt(height);
g2.setColor(getColor());
g2.setStroke(new BasicStroke(1.5f));
g2.drawLine(x1, y1, x2, y2);
}
}
public String getValidcode() {
return validcode;
}
//输出JPEG图片到前端
public static void output(BufferedImage image, OutputStream out) throws IOException {
ImageIO.write(image, "JPEG", out);
}
}
2.创建验证码过滤器
@Component
public class ValidCodeFilter extends GenericFilterBean {
@Override
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) res;
//下面代码表只过滤/login
if ("POST".equalsIgnoreCase(request.getMethod()) && "/login".equals(request.getServletPath())){
//从前端获取用户填写的验证码
String requestCode = request.getParameter("code");
String validcode = (String) request.getSession().getAttribute("validcode");
if (!validcode.toLowerCase().equals(requestCode.toLowerCase())){
//如果验证码不同就跳转
//手动设置异常
//存储错误信息,以便前端展示
request.getSession().setAttribute("msg","验证码错误");
response.sendRedirect("/toLogin");
}
}
//如果验证码相同就放行
chain.doFilter(request,response);
}
}
3.在Controller中创建方法接收验证码
@GetMapping("/validcode")
public void getValidPicture(HttpServletRequest request, HttpServletResponse response) throws IOException{
ValidCode validCode = new ValidCode();
BufferedImage image = validCode.getImage();
//获取随机验证码
String validcode = validCode.getValidcode();
System.out.println("validcode:" + validcode);
HttpSession session = request.getSession();
//将随机验证码存入session
session.setAttribute("validcode",validcode);
//输出图片
validCode.output(image,response.getOutputStream());
}
2.授权功能
2.1自定义用户授权
1.打开配置类,修改filterChain中的httpSecurity.authorizeHttpRequests()
//开启登陆配置
httpSecurity.authorizeHttpRequests()
//允许直接访问
.requestMatchers("/","/index","/validcode").permitAll()
//用户需要拥有role1的角色才能访问/menu1/**
.requestMatchers("/menu1/**").hasRole("role1")
.requestMatchers("/menu2/**").hasRole("role2")
.requestMatchers("/menu3/**").hasRole("role3")
//其他任何请求都必须经过身份验证
.anyRequest().authenticated();
2.2无访问权限的处理
1.创建errorRole.html界面
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity5">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<div>
<h1>你没有权限访问此页</h1>
<a th:href="@{/toLogin}">登录</a> <a th:href="@{/index}">返回首页</a>
</div>
</body>
</html>
2.在控制器中添加方法
@GetMapping("/errorRole")
public String errorRole(){
return "errorRole";
}
3.在配置类的filterChain方法中添加
//没有权限时跳转页
httpSecurity.exceptionHandling().accessDeniedPage("/errorRole");
2.3Thymeleaf整合Security
1.导入依赖
<dependency>
<groupId>org.thymeleaf.extras</groupId>
<artifactId>thymeleaf-extras-springsecurity6</artifactId>
</dependency>
2.在index.html文件中引入security标签
<html lang="en"
xmlns:th="http://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/extras/spring-security"
>
3.在index.html界面添加如下代码
<div>
<span sec:authorize="!isAuthenticated()">
<a th:href="@{/toLogin}">登录</a>
</span>
<span sec:authorize="isAuthenticated()">
<a th:href="@{/logout}">注销</a>
</span>
用户名:<span sec:authentication="name"></span>
</div>
4.修改各个菜单的div,通过添加Security标签从而根据用户权限决定是否显示
<div class="menu" sec:authorize="hasRole('ROLE_role1')">
<h2>menu1</h2>
<a th:href="@{/menu1/1}">menu1_1</a><br/>
<a th:href="@{/menu1/2}">menu1_2</a><br/>
<a th:href="@{/menu1/3}">menu1_3</a><br/>
</div>
<div class="menu" sec:authorize="hasRole('ROLE_role2')">
<h2>menu2</h2>
<a th:href="@{/menu2/1}">menu2_1</a><br/>
<a th:href="@{/menu2/2}">menu2_2</a><br/>
<a th:href="@{/menu2/3}">menu2_3</a><br/>
</div>
<div class="menu" sec:authorize="hasRole('ROLE_role3')">
<h2>menu3</h2>
<a th:href="@{/menu3/1}">menu3_1</a><br/>
<a th:href="@{/menu3/2}">menu3_2</a><br/>
<a th:href="@{/menu3/3}">menu3_3</a><br/>
</div>
完整代码
<!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>index</title>
<style>
.menu{
width:200px;
height: 150px;
border:1px solid #ccc;
margin-right: 20px;
float:left;
text-align: center;
}
</style>
</head>
<body>
<h1>主页</h1>
<div>
<span sec:authorize="!isAuthenticated()">
<a th:href="@{/toLogin}">登录</a>
</span>
<span sec:authorize="isAuthenticated()">
<a th:href="@{/logout}">注销</a>
</span>
用户名:<span sec:authentication="name"></span>
</div>
</br>
<div class="menu" sec:authorize="hasRole('ROLE_role1')">
<h2>menu1</h2>
<a th:href="@{/menu1/1}">menu1_1</a><br/>
<a th:href="@{/menu1/2}">menu1_2</a><br/>
<a th:href="@{/menu1/3}">menu1_3</a><br/>
</div>
<div class="menu" sec:authorize="hasRole('ROLE_role2')">
<h2>menu2</h2>
<a th:href="@{/menu2/1}">menu2_1</a><br/>
<a th:href="@{/menu2/2}">menu2_2</a><br/>
<a th:href="@{/menu2/3}">menu2_3</a><br/>
</div>
<div class="menu" sec:authorize="hasRole('ROLE_role3')">
<h2>menu3</h2>
<a th:href="@{/menu3/1}">menu3_1</a><br/>
<a th:href="@{/menu3/2}">menu3_2</a><br/>
<a th:href="@{/menu3/3}">menu3_3</a><br/>
</div>
</body>
</html>
3.使用MyBatis实现数据库认证
1.创建数据库表
2.导入相关依赖
<dependencies>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>3.0.3</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.27</version>
</dependency>
</dependencies>
3.配置数据库链接
spring:
application:
name: security3-book
security:
user:
name: admin
password: 123456
datasource:
username: root
url: jdbc:mysql://localhost:3306/securitydb?useUnicode=true&characterEncoding=utf8&serverTimezone=UTC&useSSL=true
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
mybatis:
type-aliases-package: com.example.securityBook.domain
mapper-locations: classpath:mappers/*.xml
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
server:
port: 8082
4.实体类
package com.example.securityBook.domain;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Menu {
private Integer mid;//权限编号
private String menuname;//权限名称
private String url; //该权限对应能访问的URL
private List<Role> roles; //拥有该权限的所有角色
}
package com.example.securityBook.domain;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Role {
private int rid; //角色编号
private String name; //角色英文名称
private String nameChinese; //角色中文名称
private List<Menu> menus;//一个角色可能有多个权限(菜单)
}
package com.example.securityBook.domain;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.Collection;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class TUser{
private int uid;
private String username;
private String password;
private Collection<Role> roles;//当前用户具有的角色
}
4.创建mapper
package com.example.securityBook.mapper;
import com.example.securityBook.domain.Role;
import com.example.securityBook.domain.TUser;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;
@Mapper
public interface UserMapper {
TUser findUserByName(String username); //根据姓名查找用户User
List<Role> findRolesByUserId(int id); //根据用户id查找角色Role集合,角色中又包含权限
}
<?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.example.securityBook.mapper.UserMapper">
<select id="findUserByName" resultType="TUser">
select * from user where username=#{username}
</select>
<select id="findRolesByUserId" resultMap="roleMap">
select role.*,menu.* from role,user_role,menu,role_menu
where role.rid=user_role.rid and role.rid=role_menu.rid and role_menu.mid=menu.mid and uid=#{id}
</select>
<resultMap id="roleMap" type="Role">
<id column="rid" property="rid"/>
<result column="name" property="name"/>
<result column="nameChinese" property="nameChinese"/>
<collection property="menus" ofType="Menu">
<id column="mid" property="mid"/>
<result column="menuname" property="menuname"/>
<result column="url" property="url"/>
</collection>
</resultMap>
</mapper>
5.配置UserService,实现UserDetailsService接口,重写loadUserByUsername方法
package com.example.securityBook.service;
import com.example.securityBook.domain.Menu;
import com.example.securityBook.domain.Role;
import com.example.securityBook.domain.TUser;
import com.example.securityBook.mapper.UserMapper;
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.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.Collection;
@Service
public class UserService implements UserDetailsService {
@Autowired
UserMapper userMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
TUser tuser=userMapper.findUserByName(username);//从数据库中查询用户
if(tuser==null){
throw new UsernameNotFoundException("帐户不存在");
}
tuser.setRoles(userMapper.findRolesByUserId(tuser.getUid()));//从数据库中查询到该用户的所有角色(含权限)
Collection<GrantedAuthority> authorities = new ArrayList<>();
for(Role role:tuser.getRoles()){//取出用户的角色,封装到authorities中
authorities.add(new SimpleGrantedAuthority(role.getName()));
for(Menu menu:role.getMenus()){ //如果权限精确到权限菜单级别,则要补充这个
authorities.add(new SimpleGrantedAuthority(menu.getMenuname()));
}
}
//下面的密码一般要加密
return new User(tuser.getUsername(),new BCryptPasswordEncoder().encode(tuser.getPassword()),authorities);
}
}
6.在配置类中注入UserService对象,在filterChain中添加关键代码
@Autowired
private UserService userService;
//数据库认证
httpSecurity.userDetailsService(userService);
完整代码如下
package com.example.securityBook.config;
import com.example.securityBook.filter.ValidCodeFilter;
import com.example.securityBook.service.UserService;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import java.io.IOException;
import java.io.PrintWriter;
@Configuration
public class WebSecurityConfig {
@Autowired
private ValidCodeFilter validCodeFilter;
@Autowired
private UserService userService;
@Bean
PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
//如果不想加密就返回
//return NoOpPasswordEncoder.getInstance();
}
@Bean
public WebSecurityCustomizer webSecurityCustomizer(){
//忽略这些静态资源
return(web -> web.ignoring().requestMatchers("/js/**","/css/**","/images/**"));
}
@Bean
SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception{
httpSecurity.addFilterBefore(validCodeFilter, UsernamePasswordAuthenticationFilter.class);
//开启登陆配置
httpSecurity.authorizeHttpRequests()
//允许直接访问
.requestMatchers("/","/index","/validcode").permitAll()
//用户需要拥有role1的角色才能访问/menu1/**
.requestMatchers("/menu1/**").hasRole("user")
.requestMatchers("/menu2/**").hasRole("manager")
.requestMatchers("/menu3/**").hasRole("admin")
//其他任何请求都必须经过身份验证
.anyRequest().authenticated();
//开启表单验证
httpSecurity.formLogin().loginPage("/toLogin")//跳转到自定义的登录页面
.usernameParameter("username")//自定义表单的用户名的name,默认为username
.passwordParameter("password")//自定义表单的密码的name,默认为password
.loginProcessingUrl("/login")//表单请求的地址,使用Security定义好的/login,并且与自定义表单的action一致
//.failureUrl("/toLogin/error")//如果登录失败跳转到
.permitAll()//允许访问登录有关的路径
.failureHandler(new AuthenticationFailureHandler() {
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
response.setContentType("application/json;charset = utf-8");
PrintWriter out = response.getWriter();
String json = "{\"status\":\"error\",\"msg\":\""+exception.getMessage()+"\"}";
out.write(json);
}
});
//开启注销
httpSecurity.logout().logoutSuccessUrl("/index");
//关闭csrf
httpSecurity.csrf().disable();
//记住我
httpSecurity.rememberMe();
//没有权限时跳转页
httpSecurity.exceptionHandling().accessDeniedPage("/errorRole");
//数据库认证
httpSecurity.userDetailsService(userService);
return httpSecurity.build();
}
}
注:index.html中有段角色名称也要进行相关修改,如ROLE_user替换ROLE_role1
4.使用MyBatis实现动态授权
因为访问什么资源有什么权力都是要我们手动设置的,所以我们直接从数据库查询每个角色有什么权限,将这些角色存储到Collection<ConfigAttribute>中
1.创建MenuMapper接口
package com.example.securityBook.mapper;
import com.example.securityBook.domain.Menu;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;
@Mapper
public interface MenuMapper {
//找到全部权限的集合
List<Menu> getAllMenus();
}
<?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.example.securityBook.mapper.MenuMapper">
<resultMap id="menuMap" type="Menu">
<id column="mid" property="mid"></id>
<result column="menuname" property="menuname"></result>
<result column="url" property="url"></result>
<collection property="roles" ofType="Role">
<id column="rid" property="rid"></id>
<result column="name" property="name"></result>
<result column="nameChinese" property="nameChinese"></result>
</collection>
</resultMap>
<select id="getAllMenus" resultMap="menuMap">
select menu.*,role.* from menu,role_menu,role where menu.mid=role_menu.mid and role_menu.rid=role.rid
</select>
</mapper>
2.创建工具类MyFilterInvocationSecurityMetadataSource类
package com.example.securityBook.util;
import com.example.securityBook.domain.Menu;
import com.example.securityBook.domain.Role;
import com.example.securityBook.mapper.MenuMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.ConfigAttribute;
import org.springframework.security.access.SecurityConfig;
import org.springframework.security.web.FilterInvocation;
import org.springframework.security.web.access.intercept.FilterInvocationSecurityMetadataSource;
import org.springframework.stereotype.Component;
import org.springframework.util.AntPathMatcher;
import java.util.Collection;
import java.util.List;
@Component
public class MyFilterInvocationSecurityMetadataSource implements FilterInvocationSecurityMetadataSource {
AntPathMatcher antPathMatcher=new AntPathMatcher();
@Autowired
MenuMapper menuMapper;
@Override
public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException {
String requestUrl=((FilterInvocation)object).getRequestUrl();//获取当前访问路径
List<Menu> menus=menuMapper.getAllMenus(); //获取所有的权限,每个权限又包括多个角色
for(Menu menu:menus){
if(antPathMatcher.match(menu.getUrl(),requestUrl)){ //用户访问的URL与数据库中的权限匹配
List<Role> roles=menu.getRoles(); //获取该权限对应的所有角色
String[] roleArr=new String[roles.size()];//角色集合转化为数组
for(int i=0;i<roles.size();i++){
roleArr[i]=roles.get(i).getName();
}
return SecurityConfig.createList(roleArr);//角色数组转换为Collection<ConfigAttribute>返回
}
}
return null;
}
@Override
public Collection<ConfigAttribute> getAllConfigAttributes() {
return null;
}
@Override
public boolean supports(Class<?> clazz) {
return false;
}
}
3.创建工具类MyAccessDecisionManager类
package com.example.securityBook.util;
import org.springframework.security.access.AccessDecisionManager;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.access.ConfigAttribute;
import org.springframework.security.authentication.InsufficientAuthenticationException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.stereotype.Component;
import java.util.Collection;
@Component
public class MyAccessDecisionManager implements AccessDecisionManager {
@Override
public void decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes) throws AccessDeniedException, InsufficientAuthenticationException {
Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
for(ConfigAttribute configAttribute:configAttributes){ //遍历当前url所需的全部角色
if(configAttribute.getAttribute()==null && authentication instanceof UsernamePasswordAuthenticationToken){
return; //表示已经认证过的
}
for(GrantedAuthority authority:authorities){ //遍历当前用户所有的角色权限,如果有一个能匹配上则成功
if(configAttribute.getAttribute().equals(authority.getAuthority())){
return;
}
}
}
throw new AccessDeniedException("没有权限");
}
@Override
public boolean supports(ConfigAttribute attribute) {
return false;
}
@Override
public boolean supports(Class<?> clazz) {
return false;
}
}
4.在配置类中进行配置,注入以上两个类
@Autowired
private MyFilterInvocationSecurityMetadataSource myFilterInvocationSecurityMetadataSource;
@Autowired
private MyAccessDecisionManager myAccessDecisionManager;
5.修改配置类中filterChain方法中的配置
//开启登陆配置
httpSecurity.authorizeHttpRequests()
//允许直接访问
.requestMatchers("/","/index","/validcode").permitAll()
//用户需要拥有role1的角色才能访问/menu1/**
// .requestMatchers("/menu1/**").hasRole("user")
// .requestMatchers("/menu2/**").hasRole("manager")
// .requestMatchers("/menu3/**").hasRole("admin")
.withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() {
@Override
public <O extends FilterSecurityInterceptor> O postProcess(O object) {
object.setSecurityMetadataSource(myFilterInvocationSecurityMetadataSource);
object.setAccessDecisionManager(myAccessDecisionManager);
return object;
}
})
//其他任何请求都必须经过身份验证
.anyRequest().authenticated();
完整代码
package com.example.securityBook.config;
import com.example.securityBook.filter.ValidCodeFilter;
import com.example.securityBook.service.UserService;
import com.example.securityBook.util.MyAccessDecisionManager;
import com.example.securityBook.util.MyFilterInvocationSecurityMetadataSource;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.ObjectPostProcessor;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.access.intercept.FilterSecurityInterceptor;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import java.io.IOException;
import java.io.PrintWriter;
@Configuration
public class WebSecurityConfig {
@Autowired
private MyFilterInvocationSecurityMetadataSource myFilterInvocationSecurityMetadataSource;
@Autowired
private MyAccessDecisionManager myAccessDecisionManager;
@Autowired
private ValidCodeFilter validCodeFilter;
@Autowired
private UserService userService;
@Bean
PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
//如果不想加密就返回
//return NoOpPasswordEncoder.getInstance();
}
@Bean
public WebSecurityCustomizer webSecurityCustomizer(){
//忽略这些静态资源
return(web -> web.ignoring().requestMatchers("/js/**","/css/**","/images/**"));
}
@Bean
SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception{
httpSecurity.addFilterBefore(validCodeFilter, UsernamePasswordAuthenticationFilter.class);
//开启登陆配置
httpSecurity.authorizeHttpRequests()
//允许直接访问
.requestMatchers("/","/index","/validcode").permitAll()
//用户需要拥有role1的角色才能访问/menu1/**
// .requestMatchers("/menu1/**").hasRole("user")
// .requestMatchers("/menu2/**").hasRole("manager")
// .requestMatchers("/menu3/**").hasRole("admin")
.withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() {
@Override
public <O extends FilterSecurityInterceptor> O postProcess(O object) {
object.setSecurityMetadataSource(myFilterInvocationSecurityMetadataSource);
object.setAccessDecisionManager(myAccessDecisionManager);
return object;
}
})
//其他任何请求都必须经过身份验证
.anyRequest().authenticated();
//开启表单验证
httpSecurity.formLogin().loginPage("/toLogin")//跳转到自定义的登录页面
.usernameParameter("username")//自定义表单的用户名的name,默认为username
.passwordParameter("password")//自定义表单的密码的name,默认为password
.loginProcessingUrl("/login")//表单请求的地址,使用Security定义好的/login,并且与自定义表单的action一致
//.failureUrl("/toLogin/error")//如果登录失败跳转到
.permitAll()//允许访问登录有关的路径
.failureHandler(new AuthenticationFailureHandler() {
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
response.setContentType("application/json;charset = utf-8");
PrintWriter out = response.getWriter();
String json = "{\"status\":\"error\",\"msg\":\""+exception.getMessage()+"\"}";
out.write(json);
}
});
//开启注销
httpSecurity.logout().logoutSuccessUrl("/index");
//关闭csrf
httpSecurity.csrf().disable();
//记住我
httpSecurity.rememberMe();
//没有权限时跳转页
httpSecurity.exceptionHandling().accessDeniedPage("/errorRole");
//数据库认证
httpSecurity.userDetailsService(userService);
return httpSecurity.build();
}
}
5.使用注解实现权限控制
1.在配置类中添加以下注解
@EnableGlobalMethodSecurity(prePostEnabled = true,securedEnabled = true,jsr250Enabled = true)
2.删除filterChain方法中有关手动配置的权限配置
.requestMatchers("/menu1/**").hasRole("user")
.requestMatchers("/menu2/**").hasRole("manager")
.requestMatchers("/menu3/**").hasRole("admin")
3.在控制类中添加方法,使用@Secured或者@PreAuthorize注解
jpackage com.example.securityBook.controller;
import com.example.securityBook.util.ValidCode;
import jakarta.annotation.security.DenyAll;
import jakarta.annotation.security.PermitAll;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;
import org.springframework.security.access.annotation.Secured;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.CredentialsExpiredException;
import org.springframework.security.authentication.DisabledException;
import org.springframework.security.authentication.LockedException;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.web.WebAttributes;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.ResponseBody;
import java.awt.image.BufferedImage;
import java.io.IOException;
@Controller
public class SecurityController {
//首页访问路径
@GetMapping("/index")
public String index(){
return "index";
}
@GetMapping("/toLogin")
public String tologin(HttpServletRequest request){
String msg = (String) request.getSession().getAttribute("msg");
if (msg != null){
request.setAttribute("msg",msg);
}
return "login";
}
@Secured({"ROLE_user","ROLE_manager","ROLE_admin"}) //有三种角色之一可以访问
@GetMapping("/menu1/{id}") //示例:访问menu1/1将返回menu1/1.html
public String menu1(@PathVariable("id")int id){
return "menu1/"+id;
}
@PreAuthorize("hasAnyRole('manager','admin')") //有二种角色之一可以访问
@GetMapping("/menu2/{id}") //示例:访问menu1/1将返回menu2/1.html
public String menu2(@PathVariable("id")int id){
return "menu2/"+id;
}
@PreAuthorize("hasRole('admin')") //有admin角色可以访问
@GetMapping("/menu3/{id}") //示例:访问menu1/1将返回menu3/1.html
public String menu3(@PathVariable("id")int id){
return "menu3/"+id;
}
@ResponseBody
@PreAuthorize("hasAuthority('商品管理')") //有商品管理权限可以访问
@GetMapping("/test1")
public String test1(){
return "商品管理hasAuthority";
}
@ResponseBody
@PreAuthorize("hasAuthority('用户管理')") //有用户管理权限可以访问
@GetMapping("/test2")
public String test2(){
return "用户管理hasAuthority";
}
@ResponseBody
@DenyAll
@GetMapping("/test3") //所有角色都不可访问
public String test3(){
return "DenyAll";
}
@ResponseBody
@PermitAll
@GetMapping("/test4") //所有角色都可访问
public String test4(){
return "PermitAll";
}
/*//只需要有3中角色之一便可以访问
@Secured({"ROLE_user","ROLE_manager","ROLE_admin"})
//示例,访问menu/1,将返回menu1/1.html
@GetMapping("/menu{num}/{id}")
public String menu(@PathVariable("num") int num,@PathVariable("id") int id){
return "menu"+num+"/"+id;
}*/
@GetMapping("/toLogin/error")
public String toLogin(HttpServletRequest request, Model model){
AuthenticationException authenticationException = (AuthenticationException) request.getSession().getAttribute(WebAttributes.AUTHENTICATION_EXCEPTION);
if (authenticationException instanceof UsernameNotFoundException || authenticationException instanceof BadCredentialsException){
model.addAttribute("msg","用户名或密码错误");
}else if (authenticationException instanceof DisabledException){
model.addAttribute("msg","用户名被禁用");
}else if (authenticationException instanceof LockedException){
model.addAttribute("msg","账户过期");
}else if (authenticationException instanceof CredentialsExpiredException){
model.addAttribute("msg","证书过期");
}
return "login";
}
@GetMapping("/validcode")
public void getValidPicture(HttpServletRequest request, HttpServletResponse response) throws IOException{
ValidCode validCode = new ValidCode();
BufferedImage image = validCode.getImage();
//获取随机验证码
String validcode = validCode.getValidcode();
System.out.println("validcode:" + validcode);
HttpSession session = request.getSession();
//将随机验证码存入session
session.setAttribute("validcode",validcode);
//输出图片
validCode.output(image,response.getOutputStream());
}
@GetMapping("/errorRole")
public String errorRole(){
return "errorRole";
}
}