在 WebSecurityConfig 添加@EnableGlobalMethodSecurity 注解开启方法的访问权限, 代码如下:
@EnableWebSecurity //是 Spring Security 用于启用 Web 安全的注解 @EnableGlobalMethodSecurity(prePostEnabled = true) // 开启方法级安全验证
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
//...
}
代码解释:
prePostEnabled=true 会解锁@PreAuthorize 和@PostAuthorize 两个注解,
@PreAuthorize 注解会在方法执行前进行验证,而@PostAuthorize 注解在方法执行后进行验证。
在控制层添加访问接口
在 UserController 类中增加方法的访问权限:
@RestController
public class UserController {
@Autowired
private UserInfoService userInfoService;
@GetMapping("/getUser")
public UserInfo getUser(@RequestParam String username){
return userInfoService.getUserInfo(username);
}
@PreAuthorize("hasAnyRole('user')") // 只能 user 角色才能访问该方法
@GetMapping("/user")
public String user(){
return "hello,user";
}
@PreAuthorize("hasAnyRole('admin')") // 只能 admin 角色才能访问该方法
@GetMapping("/admin")
public String admin(){
return "hello,admin";
}
}
代码解释:
@PreAuthorize("hasAnyRole('user')")注解表示访问该方法需要 USER 角色。
密码加密保存
上文中的用户密码都是手动在数据库添加的,所以数据库中是明文显示,在实际开发中, 都是需要加密保存的。下面模拟注册用户,加密保存密码:
修改 mapper 接口
在 UserMapper 接口中添加插入用户,代码如下:
@Mapper
@Repository
public interface UserMapper {
//...
@Insert("insert into user(username, password, role) value(#{username}, #{password}, #{role})")
int insertUserInfo(UserInfo userInfo);
}
修改 service 类
在 UserInfoService 类中添加插入方法,并且密码要加密保护,代码如下:
@Service
public class UserInfoService {
// ...
@Autowired
private PasswordEncoder passwordEncoder;
// ...
public int insertUser(UserInfo userInfo){
/* 加密密码*/
userInfo.setPassword(passwordEncoder.encode(userInfo.getPassword()));
return userMapper.insertUserInfo(userInfo);
}
}
修改 controoler
在 UserController 类中添加插入用户接口,代码如下:
@RestController
public class UserController{
//...
@PostMapping("/addUser")
public int addUser(@RequestBody UserInfo userInfo){
return userInfoService.insertUser(userInfo);
}
}
添加失败,响应的状态码显示 401 Unauthorized, 说明无权限,需要登录,但注册用户是不用登录的,所以需要给注册用户释放权限。
修改 WebSecurityConfig 配置类,重写 configure(HttpSecurity http)方法,配置允许注册用户的请求访问:
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
//...
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
// 允许 post 请求/addUser,而无需认证
.antMatchers(HttpMethod.POST, "/addUser").permitAll()
.anyRequest().authenticated() // 所有请求都需要验证
.and()
.formLogin() // 使用默认的登录页面
.and()
.csrf().disable();
// post 请求要关闭 csrf 验证,不然访问报错;实际开发中开启,需要前端配合传递其他参数
}}
使用加密密码登录
使用加密密码登录,需要修改 CustomUserDetailsService 类,之前从数据库拿到明文密码后需要加密,现在数据库里面的密码已经加密了,就不用加密了,代码如下:
@Component
public class MyUserDatailService implements UserDetailsService {
//...
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// ...
return new User(
userInfo.getUsername(),
// 数据库密码已加密,不用再加密
userInfo.getPassword(),
authorities
);
}
}
角色继承实际上是一个很常见的需求,因为大部分公司治理可能都是金字塔形的,上司 可能具备下属的部分甚至所有权限,这一现实场景,反映到我们的代码中,就是角色继承了。 Spring Security 中为开发者提供了相关的角色继承解决方案,只需要开发者在配置类中提供 一个 RoleHierarchy 即可,代码如下:
@Bean
RoleHierarchy roleHierarchy() {
RoleHierarchyImpl roleHierarchy = new RoleHierarchyImpl();
String hierarchy = "ROLE_dba > ROLE_admin \n ROLE_admin > ROLE_user";
roleHierarchy.setHierarchy(hierarchy);
return roleHierarchy;
}
在这里我们提供了一个 RoleHierarchy 接口的实例,使用字符串来描述了角色之间的继承关系, ROLE_dba 具备 ROLE_admin 的所有权限,而 ROLE_admin 则具备 ROLE_user的所有权限。提供了这个 Bean 之后,以后所有具备 ROLE_user 角色才能访问的资源,ROLE_dba 和 ROLE_admin 也都能访问,具备 ROLE_amdin 角色才能访问的资源,ROLE_dba 也能访问。
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.dsecurity</groupId>
<artifactId>dsecurity</artifactId>
<version>0.0.1-SNAPSHOT</version>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.0.RELEASE</version>
<relativePath /> <!-- lookup parent from repository -->
</parent>
<properties>
<!-- 声明项目配置依赖编码格式为 utf-8 -->
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<fastjson.version>1.2.24</fastjson.version>
</properties>
<dependencies>
<!--Spring Data Jpa依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<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.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.3</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
<version>8.0.13</version><!--$NO-MVN-MAN-VER$ -->
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</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>
</dependencies>
<build>
<plugins>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
</plugins>
</build>
</project>
server.port =8089
spring.datasource.url=jdbc:mysql://localhost:3306/springbootdb?serverTimezone=UTC&autoReconnect=true
spring.datasource.username=root
spring.datasource.password=admin
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
logging.level.com.example.bdatabaserole.mapper=debug
mybatis.mapper-locations=classpath:mappers/*.xml
mybatis.type-aliases-package=com.tszr.mapper
mybatis.configuration.map-underscore-to-camel-case=true
spring.jpa.database=MySQL
spring.jpa.show-sql=true
spring.jpa.hibernate.ddl-auto=create-drop
spring.jpa.database-platform=org.hibernate.dialect.MySQL5InnoDBDialect
<?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">
<!--命名空间必须和UserMapper全类名相同 -->
<mapper namespace="com.tszr.mapper.UserMapper">
<select id="getUserByUsername"
resultType="com.tszr.entity.User">
select * from user where username = #{username};
</select>
<select id="getRolesById" resultType="com.tszr.entity.Role">
select * from role where id in(select rid from user_role where uid=#{uid});
</select>
</mapper>
package com.tszr.entity;
import javax.persistence.CascadeType;
import javax.persistence.Column;
import javax.persistence.JoinColumn;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.ManyToOne;
import javax.persistence.Table;
@Entity
@Table(name = "role")
public class Role {
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getNameZh() {
return nameZh;
}
public void setNameZh(String nameZh) {
this.nameZh = nameZh;
}
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@ManyToOne(cascade=CascadeType. REFRESH ,optional= false)
@ JoinColumn (name = "user_id" )
private int id;
@Column(name = "name", unique = true, nullable = false, length = 64)
private String name;
@Column(name = "nameZh", nullable = false, length = 64)
private String nameZh;
}
package com.tszr.entity;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import com.fasterxml.jackson.annotation.JsonIgnore;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Table;
@Entity
@Table(name = "user")
public class User implements Serializable, UserDetails {
private static final long serialVersionUID = 1L;
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private int id;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public List<Role> getRoleList() {
return roleList;
}
public void setRoleList(List<Role> roleList) {
this.roleList = roleList;
}
@Column(name = "username", unique = true, nullable = false, length = 64)
private String username;
@Column(name = "password", nullable = false, length = 64)
private String password;
@JsonIgnore
private List<Role> roleList;
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
Collection<GrantedAuthority> authorities = new ArrayList<>();
for (Role role : roleList) {
// 数据库role表字段中是以ROLE_开头的,所以此处不必再加ROLE_
authorities.add(new SimpleGrantedAuthority(role.getName()));
}
return authorities;
}
/**
* 指示用户的账户是否已过期。无法验证过期的账户。 如果用户的账户有效(即未过期),则返回true,如果不在有效就返回false
*/
@Override
public boolean isAccountNonExpired() {
return true;
}
/**
* 指示用户是锁定还是解锁。无法对锁定的用户进行身份验证。 如果用户未被锁定,则返回true,否则返回false
*/
@Override
public boolean isAccountNonLocked() {
return true;
}
/**
* 指示用户的凭证(密码)是否已过期。过期的凭证阻止身份验证 如果用户的凭证有效(即未过期),则返回true 如果不在有效(即过期),则返回false
*/
@Override
public boolean isCredentialsNonExpired() {
return true;
}
/**
* 指示用户是启用还是禁用。无法对禁用的用户进行身份验证 如果启用了用户,则返回true,否则返回false
*/
@Override
public boolean isEnabled() {
return true;
}
}
package com.tszr.mapper;
import java.util.List;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Mapper;
import org.springframework.stereotype.Repository;
import com.tszr.entity.Role;
import com.tszr.entity.User;
@Mapper
@Repository
public interface UserMapper {
// @Select("select * from user where username = #{username}")
User getUserByUsername(String username);
List<Role> getRolesById(int id);
// 添加用户
@Insert("insert into user(username, password) value(#{username}, #{password})")
int insertUserInfo(User user);
}
package com.tszr.services;
import org.springframework.stereotype.Service;
import com.tszr.entity.User;
import com.tszr.mapper.UserMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.crypto.password.PasswordEncoder;
@Service
public class UserInfoService {
@Autowired
private UserMapper userMapper;
@Autowired
private PasswordEncoder passwordEncoder;
public int insertUser(User userInfo) {
// 加密密码
userInfo.setPassword(passwordEncoder.encode(userInfo.getPassword()));
return userMapper.insertUserInfo(userInfo);
}
public User getUserInfo(String username) {
return userMapper.getUserByUsername(username);
}
}
package com.tszr.services;
import com.tszr.entity.User;
import com.tszr.mapper.UserMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
public class CustomUserDetailsService implements UserDetailsService {
@Autowired
private UserInfoService userInfoService;
@Autowired
private UserMapper userMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 通过用户名从数据库获取用户信息
User userInfo = userInfoService.getUserInfo(username);
if (userInfo == null) {
throw new UsernameNotFoundException("用户不存在");
}
userInfo.setRoleList(userMapper.getRolesById(userInfo.getId()));
return userInfo;
}
}
package com.tszr.controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize;
import com.tszr.entity.User;
import com.tszr.services.UserInfoService;
@RestController
public class UserController {
@Autowired
private UserInfoService userInfoService;
// 添加
@PostMapping("/addUser")
public int addUser(@RequestBody User userInfo) {
return userInfoService.insertUser(userInfo);
}
@GetMapping("/getUser")
public User getUser(@RequestParam String username) {
return userInfoService.getUserInfo(username);
}
@PreAuthorize("hasAnyRole('user')") // 只能user角色才能访问该方法
@GetMapping("/user")
public String user() {
return "hello,user";
}
@PreAuthorize("hasAnyRole('dba','admin')") // dba\admin角色可以访问该方法
@GetMapping("/db")
public String dba() {
return "hello,dba,admin";
}
@PreAuthorize("hasAnyRole('admin')") // 只能admin角色才能访问该方法
@GetMapping("/admin")
public String admin() {
return "hello,admin";
}
}
package com.tszr.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
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.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import com.tszr.services.CustomUserDetailsService;
@EnableWebSecurity // 是Spring Security用于启用Web安全的注解
@EnableGlobalMethodSecurity(prePostEnabled = true) // 开启方法级安全验证
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private CustomUserDetailsService userDatailService;
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().antMatchers(HttpMethod.POST, "/addUser").permitAll() // 允许post请求/add-user,而无需认证
.anyRequest().authenticated() // 所有请求都需要验证
.and().formLogin() // 使用默认的登录页面
.and().csrf().disable();// post请求要关闭csrf验证,不然访问报错;实际开发中开启,需要前端配合传递其他参数
}
/**
* 指定加密方式
*/
@Bean
public PasswordEncoder passwordEncoder() {
// 使用BCrypt加密密码
return new BCryptPasswordEncoder();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// 从数据库读取的用户进行身份认证
auth.userDetailsService(userDatailService).passwordEncoder(passwordEncoder());
}
}
package com.tszr;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}