SpringSecurity
前言
最近做的一个项目需要用到权限控制,抽空浅学了一下SpringSecurity这个权限控制框架,分享一些经验和大家共同学习一下
实现
本文章主要通过一个小小的前后端分离demo来说一下SpringSecurity具体使用
以下完整代码已上传到 GitHub:https://github.com/Fjz-Kuroko/SpringSecurityDemo
1、准备工作
数据库设计
一般有关权限控制的数据库设计都会比较细,比较全面的,往往会包括:
- 用户表
- 角色表
- 权限表
- 用户和角色关系表
- 角色和权限关联表
我这里为了简单演示就简化了数据库的设计,只用了一个用户表
添加pom依赖
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.6.3</version>
<relativePath/>
</parent>
<dependencies>
<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>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.2.0</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</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>
配置一下数据库连接和MyBatis-plus
spring:
datasource:
username: root
password: 87654321
url: jdbc:mysql://localhost:3306/tedu?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai
driver-class-name: com.mysql.cj.jdbc.Driver
application:
name: springsecuritydemo
mybatis-plus:
mapper-locations: classpath*:/mapper/**/*.xml
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
2、编码
基本的实体类、mapper、service的编写在这里就不赘述了,这里主要写怎么使用SpringSecurity
1、 建立自定义的UserDetailsService
UserDetailsService是SpringSecurity中用来获取用户细节的接口,也是用户认证的核心逻辑
这里的UserDetails loadUserByUsername(String username) throws UsernameNotFoundException; 方法中的参数就是我们登录时提交的用户名,在这里必须构造出一个UserDetails 的实现类User (这里的User是指SpringSecurity提供的User,不是我们自定义的User)
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
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.stereotype.Service;
import xyz.fjzkuroko.springsecuritydemo.dao.UsersDAO;
import xyz.fjzkuroko.springsecuritydemo.entity.Users;
import java.util.ArrayList;
import java.util.List;
@Service("userDetailsService")
public class CustomUserDetailsService implements UserDetailsService {
@Autowired
private UsersDAO usersDAO;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
Users users = usersDAO.selectOne(new QueryWrapper<Users>().eq("username", username));
if (users == null) {
throw new UsernameNotFoundException("用户名不存在!");
}
List<GrantedAuthority> grantedAuthorities = new ArrayList<>();
grantedAuthorities.add(new SimpleGrantedAuthority("ROLE_" + users.getRole()));
return new User(users.getUsername(), users.getPassword(), grantedAuthorities);
}
}
2、 自定义密码加密类
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Component;
import xyz.fjzkuroko.springsecuritydemo.utils.MD5Util;
@Component
public class CustomPasswordEncoder implements PasswordEncoder {
@Override
public String encode(CharSequence rawPassword) {
return MD5Util.getMD5(rawPassword.toString(), MD5Util.LENGTH_32);
}
@Override
public boolean matches(CharSequence rawPassword, String encodedPassword) {
return encodedPassword.equals(MD5Util.getMD5(rawPassword.toString(), MD5Util.LENGTH_32));
}
}
3、自定义登录成功、登出成功、无权访问等等处理器
因为我们现在基本都是前后端分离的项目了,我们应该在各个操作完成后给前端返回一个json数据,而不是跳转页面
Spring Security本身封装好了登陆、登出的接口(我们也可以自定义)。默认登入路径:/login,登出路径:/logout。 当登陆成功或登陆失败都须要返回统一的json返回体给前台,前台才能知道对应的作什么处理。
1.自定义登录成功处理器
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.stereotype.Component;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
@Component
public class CustomAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication)
throws IOException, ServletException {
response.setContentType("application/json;charset=utf-8");
PrintWriter writer = response.getWriter();
writer.write("{\"status\":\"success\",\"msg\":\"登陆成功\"}");
writer.flush();
writer.close();
}
}
2.自定义登录失败处理器
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.stereotype.Component;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
@Component
public class CustomAuthenticationFailHandler implements AuthenticationFailureHandler {
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception)
throws IOException, ServletException {
response.setContentType("application/json;charset=utf-8");
PrintWriter writer = response.getWriter();
writer.write("{\"status\":\"error\",\"msg\":\"登陆失败\"}");
writer.flush();
writer.close();
}
}
3. 自定义退出成功处理器
@Component
public class CustomLogoutSuccessHandler implements LogoutSuccessHandler {
@Override
public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
response.setContentType("application/json;charset=utf-8");
PrintWriter writer = response.getWriter();
writer.write("{\"status\":\"success\",\"msg\":\"退出成功\"}");
writer.flush();
writer.close();
}
}
4.自定义无权访问处理器
@Component
public class CustomAccessDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
response.setContentType("application/json;charset=utf-8");
PrintWriter writer = response.getWriter();
writer.write("{\"status\":\"forbidden\",\"msg\":\"权限不足\"}");
writer.flush();
writer.close();
}
}
3、配置WebSecurityConfig
具体的配置看注释吧,写得很清楚了
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private CustomUserDetailsService customUserDetailsService;
@Autowired
private CustomAuthenticationSuccessHandler customAuthenticationSuccessHandler;
@Autowired
private CustomAuthenticationFailHandler customAuthenticationFailHandler;
@Autowired
private CustomAccessDeniedHandler customAccessDeniedHandler;
@Autowired
private CustomLogoutSuccessHandler customLogoutSuccessHandler;
@Autowired
private CustomAuthenticationEntryPoint customAuthenticationEntryPoint;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(customUserDetailsService).passwordEncoder(new CustomPasswordEncoder());
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.httpBasic()
.authenticationEntryPoint(customAuthenticationEntryPoint)
.and()
.authorizeRequests()
.antMatchers("/test/**", "**/b").permitAll()
.antMatchers("/test/admin").hasRole("admin")
.anyRequest().authenticated()
.and()
.formLogin()
.successHandler(customAuthenticationSuccessHandler)
.failureHandler(customAuthenticationFailHandler)
.loginProcessingUrl("/login")
.usernameParameter("username")
.passwordParameter("password")
.permitAll()
.and()
.logout()
.permitAll()
.logoutSuccessHandler(customLogoutSuccessHandler)
.and()
.exceptionHandling()
.accessDeniedHandler(customAccessDeniedHandler)
.and()
.cors()
.and()
.csrf().disable();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
实际上SpringSecurity还内置其他很多访问控制方法,这里就不一一去介绍了,大家有兴趣的去搜一下吧~
|