项目架构:
SpringBoot 工程
springboot版本 2.1.3
SSM 架构 + Mysql 数据库 + Maven
前端界面 使用的是jsp 并引用了 bootstrap
一:引入Spring Security安全框架
pom.xml 加入引用
<!-- Spring Security 权限管理框架 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
二:默认拦截
引入Spring Security安全框架 ,
启动项目后,访问项目 会自动拦截到默认的登录界面(自带的)
在控制台和日志中会出现默认密码, 默认帐号user
登录后,跳转到真实访问的界面。
引入spring Security 后,不想开启 拦截校验,可以在项目主方法上加上
exclude= SecurityAutoConfiguration.class
//关闭Security 登录验证功能,Security5.x 后设置
@SpringBootApplication(exclude= SecurityAutoConfiguration.class)
三:自定义各类拦截
实现自定义拦截的关键在于 WebSecurityConfigurerAdapter 类
此类提供各类的配置方法
//WebSecurityConfigurerAdapter 类 部分源码
public abstract class WebSecurityConfigurerAdapter implements
WebSecurityConfigurer<WebSecurity> {
//由默认实现{@link #authenticationManager()},可以查看源码的注释,对理解使用有很大帮助 eg:实现权限控制,校验帐号密码
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
this.disableLocalConfigureAuthenticationBldr = true;
}
//重写此方法来配置{@link WebSecurity}。eg:忽略某些请求
public void configure(WebSecurity web) throws Exception {
}
//覆盖这个方法来配置{@link HttpSecurity},eg:通过重写,配置http来进行拦截的配置
protected void configure(HttpSecurity http) throws Exception {
logger.debug("Using default configure(HttpSecurity). If subclassed this will potentially override subclass configure(HttpSecurity).");
http
.authorizeRequests()
.anyRequest().authenticated()
.and()
.formLogin().and()
.httpBasic();
}
}
首先新建继承 WebSecurityConfigurerAdapter 的配置类
1.基于内存的方式 来拦截
@Configuration
@EnableWebSecurity//开启Spring Security的功能
//prePostEnabled属性决定Spring Security在接口前注解是否可用
//@PreAuthorize,@PostAuthorize等注解,设置为true,
//会拦截加了这些注解的接口
@EnableGlobalMethodSecurity(prePostEnabled=true)
public class WebSecurityConfg extends WebSecurityConfigurerAdapter{
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//super.configure(auth);
System.out.println("WebSecurityConfg AuthenticationManagerBuilder");
// 基于内存的方式,
//创建两个用户admin/123456 ,user/123456 可以使用这两个用户登录
auth.inMemoryAuthentication()
.withUser("admin")//用户名
.password(passwordEncoder().encode("123456"))//密码
.roles("ADMIN");//角色
auth.inMemoryAuthentication()
.withUser("user")//用户名
.password(passwordEncoder().encode("123456"))//密码
.roles("USER");//角色
}
@Bean
public PasswordEncoder passwordEncoder(){
// 使用BCrypt加密密码
return new BCryptPasswordEncoder();
}
}
2.查询数据库 用户数据进行 拦截
(1)最关键的是实现 org.springframework.security.core.userdetails.UserDetailsService 接口
加载用户特定数据的核心接口。
它作为一个用户DAO在整个框架中使用,也是DaoAuthenticationProvider使用的策略。
该接口只需要一个只读方法,这简化了对新的数据访问策略的支持。
import java.util.ArrayList;
import java.util.List;
import javax.annotation.Resource;
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.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import com.myjz.clansman.entity.user.UserAndRole;
import com.myjz.clansman.entity.user.UserInfo;
import com.myjz.clansman.service.user.UserService;
@Service
public class CustomUserDetailsService implements UserDetailsService{
@Resource
private UserService userService;
@Resource
private PasswordEncoder passwordEncoder;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
System.out.println("校验 登录------");
/**
* 1/通过userName 获取到userInfo信息,这里的username 就是帐号
* 2/通过User(UserDetails)返回details。
*/
//通过userName获取用户信息
UserInfo userInfo = userService.queryUserByAccount(username);
if(userInfo == null) {
throw new UsernameNotFoundException("not found");
}
//查询权限列表
List<UserAndRole> roleList = userService.queryUserAndRoleList(userInfo.getUserId());
//定义权限列表.
List<GrantedAuthority> authorities = new ArrayList<>();
// 用户可以访问的资源名称(或者说用户所拥有的权限) 注意:必须"ROLE_"开头
for(UserAndRole useRoleId : roleList) {
GrantedAuthority authoritie = new SimpleGrantedAuthority("ROLE_"+useRoleId.getRoleId());
authorities.add(authoritie);
}
User userDetails = new User(userInfo.getAccount(),passwordEncoder.encode(userInfo.getPassword()),authorities);
return userDetails;
}
}
(2)对WebSecurityConfg 进行配置
@Configuration
@EnableWebSecurity//开启Spring Security的功能
//prePostEnabled属性决定Spring Security在接口前注解是否可用
//@PreAuthorize,@PostAuthorize等注解,设置为true,
//会拦截加了这些注解的接口
@EnableGlobalMethodSecurity(prePostEnabled=true)
public class WebSecurityConfg extends WebSecurityConfigurerAdapter{
@Autowired
CustomUserDetailsService userDetailsService;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//加载userDetailsService
auth.userDetailsService(userDetailsService).passwordEncoder(new BCryptPasswordEncoder());
}
}
3.自定义拦截(自定义登录界面,登录登出拦截器等)-form表单提交
(1).对WebSecurityConfg 进行配置
import org.slf4j.LoggerFactory;
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.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 org.springframework.security.web.util.matcher.AntPathRequestMatcher;
/**
* SpringSecurity安全框架配置
*/
@Configuration
@EnableWebSecurity//开启Spring Security的功能
//prePostEnabled属性决定Spring Security在接口前注解是否可用
//@PreAuthorize,@PostAuthorize等注解,设置为true,
//会拦截加了这些注解的接口
@EnableGlobalMethodSecurity(prePostEnabled=true)
public class WebSecurityConfg extends WebSecurityConfigurerAdapter{
@Autowired
CustomUserDetailsService userDetailsService;
org.slf4j.Logger log = LoggerFactory.getLogger(WebSecurityConfg.class);
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//super.configure(auth);
System.out.println("WebSecurityConfg AuthenticationManagerBuilder");
auth.userDetailsService(userDetailsService).passwordEncoder(new BCryptPasswordEncoder());
}
@Autowired
LoginSuccessHandler loginSuccessHandler;
@Autowired
LoginFailureHandler loginFailureHandler;
//http请求拦截配置
@Override
protected void configure(HttpSecurity http) throws Exception {
System.out.println("Using configure(HttpSecurity)");
http//配置权限认证
.authorizeRequests()
//配置不拦截路由
.antMatchers("/css/**", "/js/**","/images/**").permitAll()//不拦截资源文件,否则样式,图片等 无法使用
.antMatchers("/main/500").permitAll()
.antMatchers("/main/403").permitAll()
.antMatchers("/main/404").permitAll()
.anyRequest()//任何请求
.authenticated();//需要身份验证
http.formLogin()//自定义表单登录
.loginPage("/login/index").permitAll()//自定义登录界面的url,上面需要不拦截 自定义的url
.usernameParameter("username")//设置帐号参数,与表单参数一致
.passwordParameter("password")//设置密码参数,与表单参数一致
// 告诉Spring Security在发送指定路径时处理提交的凭证,默认情况下,将用户重定向回用户来自的页面。登录表单form中action的地址,也就是处理认证请求的路径,
// 只要保持表单中action和HttpSecurity里配置的loginProcessingUrl一致就可以了,也不用自己去处理,它不会将请求传递给Spring MVC和您的控制器,所以我们就不需要自己再去写一个/login/doLogin的控制器接口了
.loginProcessingUrl("/login/doLogin")//配置默认登录入口
.successHandler(loginSuccessHandler)//自定义校验成功处理器
.failureHandler(loginFailureHandler);//自定义检验失败处理器
//自定义注销
http.logout()
.logoutUrl("/login/doLogout")//配置注销入口 (与登录入口一样,只需表单中action 和此处的一致即可)
.logoutSuccessHandler(new CustomLogoutSuccessHandler());//注销成功处理器
// 关闭csrf防护
//http.csrf().disable();
}
/**
* 指定加密方式
*/
@Bean
public PasswordEncoder passwordEncoder(){
// 使用BCrypt加密密码
return new BCryptPasswordEncoder();
}
}
(2).新建 登录成功处理器
import java.io.IOException;
import java.util.Enumeration;
import javax.annotation.Resource;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.http.HttpStatus;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler;
import org.springframework.security.web.savedrequest.RequestCache;
import org.springframework.stereotype.Component;
import com.fasterxml.jackson.databind.ObjectMapper;
/**
* @Description 登录成功处理器
*/
@Component("loginSuccessHandler")
public class LoginSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {
@Override
public void onAuthenticationSuccess(HttpServletRequest request,
HttpServletResponse response, Authentication authentication)
throws ServletException, IOException {
System.out.println("LoginSuccessHandler");
//获得前端传到后端的全部参数
Enumeration enu = request.getParameterNames();
while (enu.hasMoreElements()) {
String paraName = (String) enu.nextElement(); System.out.println("参数- " + paraName + " : " + request.getParameter(paraName));
}
logger.info("登录认证成功");
//重定向跳转路径
this.getRedirectStrategy().sendRedirect(request, response, "/main/main");
}
}
(3). 登录失败处理器
import java.io.IOException;
import javax.annotation.Resource;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.security.authentication.AccountExpiredException;
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.security.web.authentication.SimpleUrlAuthenticationFailureHandler;
import org.springframework.stereotype.Component;
import com.fasterxml.jackson.databind.ObjectMapper;
/**
* @Description 登录失败处理器
*/
@Component("loginFailureHandler")
public class LoginFailureHandler extends SimpleUrlAuthenticationFailureHandler {
public void onAuthenticationFailure(HttpServletRequest request,
HttpServletResponse response, AuthenticationException exception)
throws IOException, ServletException {
logger.info("登录失败 处理器");
//以下写登录失败的逻辑
String errorInfo = "";
if (exception instanceof BadCredentialsException ||
exception instanceof UsernameNotFoundException) {
errorInfo = "账户名或者密码输入错误!";
} else if (exception instanceof LockedException) {
errorInfo = "账户被锁定,请联系管理员!";
} else if (exception instanceof CredentialsExpiredException) {
errorInfo = "密码过期,请联系管理员!";
} else if (exception instanceof AccountExpiredException) {
errorInfo = "账户过期,请联系管理员!";
} else if (exception instanceof DisabledException) {
errorInfo = "账户被禁用,请联系管理员!";
} else {
errorInfo = "登录失败!";
}
logger.info("登录失败原因:" + errorInfo);
//System.out.println("错误提示:"+exception.getMessage());
request.getSession().setAttribute("loginError",errorInfo);
//表单提交
this.saveException(request, exception);
//重定向跳转路径
this.getRedirectStrategy().sendRedirect(request, response, "/login/index");
}
}
(4).注销登录处理器
import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.slf4j.LoggerFactory;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.web.DefaultRedirectStrategy;
import org.springframework.security.web.RedirectStrategy;
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
import org.springframework.stereotype.Component;
/**
* 注销登录
*/
@Component
public class CustomLogoutSuccessHandler implements LogoutSuccessHandler {
org.slf4j.Logger log = LoggerFactory.getLogger(CustomLogoutSuccessHandler.class);
private RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();
@Override
public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication)
throws IOException, ServletException {
System.out.println("注销成功!");
User user = (User) authentication.getPrincipal();
String username = user.getUsername();
log.info("username: {} is offline now", username);
//重定向跳转路径
redirectStrategy.sendRedirect(request, response, "/login/index");
}
}
(5).登录控制器Controller
@RestController
@RequestMapping("/login")
public class LoginController {
//跳转登录界面
@RequestMapping(value="/index",method=RequestMethod.GET)
public ModelAndView index(){
ModelAndView view = new ModelAndView();
view.setViewName("/login");
return view;
}
}
(6). 新建登录界面 login.jsp 以及 控制跳转
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt"%>
<!DOCTYPE html>
<html>
<head>
<title>登录页</title>
<myLink>
<link href="${pageContext.request.contextPath}/css/signin.css"
rel="stylesheet">
</myLink>
</head>
<body>
<div class="container">
<c:url value="/login/doLogin" var="loginUrl" />
<form class="form-signin" method="post" action="${loginUrl}">
<!-- csrf防护未关闭时,需要添加 -->
<input type="hidden" name="${_csrf.parameterName}"
value="${_csrf.token}" />
<h2 class="form-signin-heading">请登录</h2>
<label for="inputAccount" class="sr-only">用户名</label> <input
name="username" type="text" id="inputAccount" class="form-control"
placeholder="用户名" required autofocus> <label
for="inputPassword" class="sr-only">密码</label> <input
name="password" type="password" id="inputPassword"
class="form-control" placeholder="密码" required>
<div class="checkbox">
<label> <input type="checkbox" value="remember-me">
Remember me
</label>
</div>
<span>${loginError}</span>
<button class="btn btn-lg btn-primary btn-block" type="submit">登录</button>
</form>
</div>
</body>
</html>
(7).配置错误页面 403 404 500 适用于 SpringBoot 2.x
只要实现下面的类即可
@Configuration
public class ErrorPageConfig {
@Bean
public WebServerFactoryCustomizer<ConfigurableWebServerFactory> webServerFactoryCustomizer() {
WebServerFactoryCustomizer<ConfigurableWebServerFactory> webCustomizer = new WebServerFactoryCustomizer<ConfigurableWebServerFactory>() {
@Override
public void customize(ConfigurableWebServerFactory factory) {
System.out.println("ErrorPageConfig------");
ErrorPage[] errorPages = new ErrorPage[] {
new ErrorPage(HttpStatus.FORBIDDEN, "/main/403"),
new ErrorPage(HttpStatus.NOT_FOUND, "/main/404"),
new ErrorPage(HttpStatus.INTERNAL_SERVER_ERROR, "/main/500"),
};
factory.addErrorPages(errorPages);
}
};
return webCustomizer;
}
}
很多坑的点和异常 都在代码中有标记 大家要注意 以下特别提醒
- http loginPage参数 配置时 一定要加permitAll() 忽略拦截 不然登录界面出不来
- http usernameParameter 和passwordParameter 参数 配置时 一定要和界面的name 参数保持一致
- http loginProcessingUrl参数 配置 保持表单中action一致,这里还有个点,刚开始我并没有使用jstl 的<c:url> 标签,直接放了action=“/login/doLogin”,其实url标签默认加了访问路径,如果不用<c:url> 标签 ,必须 action=“${pageContext.request.contextPath}/login/doLogin” 才可以,注销登录同理
//自定义配置中注销的配置
http.logout()
.logoutUrl("/login/doLogout")//配置注销入口 (与登录入口一样,只需表单中action 和此处的一致即可)
.logoutSuccessHandler(new CustomLogoutSuccessHandler());//注销成功处理器
//注销登录 的界面写法
<form action="${pageContext.request.contextPath}/login/logout" method="post" >
<input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}"/>
<button type="submit">退出1</button>
</form>
或者
引入 jstl 标签
<c:url value="/login/logout" var="logoutUrl"/>
<form action="${logoutUrl}" method="post" >
<input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}"/>
<button type="submit">退出1</button>
</form>
- 关闭csrf防护 如果不关闭防护的话 登录界面中form 表单必须加上 csrf 的参数
<input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}"/>
- 界面 的样式 都没生效,F12 看浏览器开发工具,发现报错
“because its MIME type ('text/html') is not executable, and strict MIME type checking is enabled”
问题在于我原来配置中没有将 “/js/" ,"/css/” 的路径添加到配置中,导致没有验证的用户没有权限访问
.antMatchers("/css/**", "/js/**","/images/**").permitAll()
权限校验还未添加 未完待续…
引用并参考以下文章
官方中文文档: https://www.springcloud.cc/spring-reference.html Spring
security API 文档:https://docs.spring.io/spring-security/site/docs/5.0.0.M3/api/
官方中文说明文档:https://www.springcloud.cc/spring-security.html