1、引入依赖
<!-- spring-srcrity 安全框架-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!--验证码-->
<dependency>
<groupId>com.github.penggle</groupId>
<artifactId>kaptcha</artifactId>
<version>2.3.2</version>
</dependency>
2、编写配置类OaSecurityConfig
package com.manager.oa.config;
import com.manager.oa.filter.CaptchaFilter;
import com.manager.oa.filter.ValidateException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
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.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
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.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* @Author sms
* @Version V1.0.0
* @Date 2022-08-17
*/
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true) // 启用权限验证
public class OaSecurityConfig extends WebSecurityConfigurerAdapter {
/**
* 配置验证码过滤器
*
* @return
*/
@Bean
public CaptchaFilter captchaFilter() {
return new CaptchaFilter();
}
/**
* spring security 配置类
*
* @param http
* @throws Exception
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
// 验证码过滤器,加在了用户密码过滤器之前
http.addFilterBefore(captchaFilter(), UsernamePasswordAuthenticationFilter.class);
http.formLogin()
.loginPage("/portal/login") //指定自己的登录页面
.loginProcessingUrl("/portal/tologin") //与登录表单的action保持一致
// .successForwardUrl("/portal/")
.successHandler(new AuthenticationSuccessHandler() { // 成功定制
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
response.sendRedirect("/portal/"); // 从定向
// response.getWriter().print(""); // 返回前端JSON
}
})
// .failureForwardUrl("/portal/login") // 失败后的url
.failureHandler(authenticationFailureHandler()) // 失败后进行异常处理
.usernameParameter("account")
.passwordParameter("password")
.and()
.authorizeRequests() //下面的都是授权的配置 3
.antMatchers("/portal/login", "/css/**", "/images/*", "/captcha").permitAll()
.anyRequest() //任何请求 4
.authenticated()//要求认证
.and().csrf().disable(); //禁用csrf攻击解决方案
http.headers().frameOptions().disable()
.and()
.logout();
// 在没有权限访问时,SpriSecurity会抛出AccessDeniedException,为了处理该异常,使用以下配置
http.exceptionHandling().accessDeniedHandler((req, resp, e) -> {
resp.sendRedirect("/portal/noperm");
});
// 在登录成功之后,如果会话失效(默认30分钟或手动删除Cookie),默认跳到登录界面,如果需要自定义,
http.exceptionHandling().authenticationEntryPoint((req, resp, e) -> {
resp.sendRedirect("/portal/nosession");
});
}
@Autowired
private UserDetailsService userDetailsService;
/**
* 异常处理Bean的配置
* 由于框架在验证密码和用户名时,做了相关的模糊处理,默认提示“用户名或密码错误”,
* 需要将HideUserNotFoundExceptions设置为false,就可以分别提示,用户名,密码错误;
* <p>
* 由于 new DaoAuthenticationProvider();之后,里面的属性都为空,需要重新将userDetailsService 设置进去
*
* @return
*/
@Bean
public DaoAuthenticationProvider daoAuthenticationProvider() {
DaoAuthenticationProvider daoAuthenticationProvider = new DaoAuthenticationProvider();
// 为了提供细致提示,设置Provider的hideUserNotFoundExceptions的属性为false
daoAuthenticationProvider.setHideUserNotFoundExceptions(false);
daoAuthenticationProvider.setUserDetailsService(userDetailsService);
daoAuthenticationProvider.setPasswordEncoder(passwordEncoder());
return daoAuthenticationProvider;
}
/**
* 异常处理Bean
*
* @return
*/
@Bean
public AuthenticationFailureHandler authenticationFailureHandler() {
return (req, resp, exception) -> {
if (exception instanceof UsernameNotFoundException) {
req.setAttribute("msg", "账号不存在");
} else if (exception instanceof BadCredentialsException) {
req.setAttribute("msg", "密码不正确");
} else if (exception instanceof ValidateException) {
req.setAttribute("msg", "验证码不正确");
} else {
req.setAttribute("msg", "系统维护中");
}
req.getRequestDispatcher("/portal/login").forward(req, resp);
};
}
/**
* 密码加密
*
* @return
*/
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
3、编写自己的OaUserDetailsService,实现框架的UserDetailsService
如下:
框架中的UserDetailsService,里面只有一个方法,功能是通过用户名来验证用户;
如果找不到用户,或者用户不存在,则抛出UsernameNotFoundException;
所以我们需要实现该方法首先验证用户名是否正确
由于该方法需要返回一个UserDetails(框架中的);
该UserDetails主要存在以下属性,显然,这些都是一些基本属性,由于我们还要做权限验证,这些属性是不够的,
所以我们需要编写自己的user类,来实现该类,增强一些属性:
编写:OaUser
package com.manager.oa.pojo.oa;
import lombok.Data;
import lombok.Getter;
import lombok.Setter;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.User;
import java.util.Collection;
/**
* @Author sms
* @Version V1.0.0
* @Date 2022-08-18
*/
@Getter
@Setter
public class OaUser extends User {
public OaUser(String username, String password, Collection<? extends GrantedAuthority> authorities) {
super(username, password, authorities);
}
public OaUser(String username, String password, com.manager.oa.pojo.rbac.User user, Collection<? extends GrantedAuthority> authorities) {
this(username, password, authorities);
this.user = user;
}
// 新增属性,返回自己的整个实体类中的所有属性
private com.manager.oa.pojo.rbac.User user;
}
由于麻烦。我直接新增了我自己的use实体类
以下是我的User类
@Data
@AllArgsConstructor
@NoArgsConstructor
@JsonIgnoreProperties(value = {"handler"})
public class User {
private Integer id;
private String account; // 账号
private String password; // 密码
private String img;// 头像
private Role role;// 角色
private String state;// 状态
private Integer roleId;// 角色ID
}
现在编写OaUserDetailsService,重新 loadUserByUsername 该方法返回的就是我们刚刚编写的OaUser 该类是实现了框架的UserDetails的;
返回的参数作简要说明:
第一个参数:username,就是我们需要验证的账号,也就是前端传过来的用户名
第二个参数:password, 为数据库中查询到的该账号的密码
第三个参数:data, 为需要返回的数据,(我这里直接返回数据库中user的实体类)
第四个参数:list,为该用户的所有权限,为后面做权限验证准备
package com.manager.oa.service.rbac.impl;
import com.manager.oa.mapper.rbac.UserMapper;
import com.manager.oa.pojo.oa.OaUser;
import com.manager.oa.pojo.rbac.Permission;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
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.Component;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
/**
* @Author sms
* @Version V1.0.0
* @Date 2022-08-17
*/
@Slf4j
@Component
public class OaUserDetailsService implements UserDetailsService {
@Autowired
private UserMapper userMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//根据账号去查询用户信息,把用户的权限查出来
log.debug("用户账号:{}", username);
com.manager.oa.pojo.rbac.User user = new com.manager.oa.pojo.rbac.User();
user.setAccount(username);
List<com.manager.oa.pojo.rbac.User> users = userMapper.findUsersByCondition(user);
if (users.size() == 0) {//避免空指针
throw new UsernameNotFoundException("账号不存在"); //抛出异常
}
log.debug("数据库信息:{}", users.get(0));
log.debug("用户权限信息:{}", users.get(0).getRole().getPermissions());
List<SimpleGrantedAuthority> list = new ArrayList<>();
for (Permission p : users.get(0).getRole().getPermissions()
) {
//过滤父权限
if (p.getPerPower() == null) {
continue;
}
SimpleGrantedAuthority simpleGrantedAuthority = new SimpleGrantedAuthority(p.getIdentify());
list.add(simpleGrantedAuthority);
}
return new OaUser(username, users.get(0).getPassword(), users.get(0), list);
}
}
以下是我的 UserMapper,由于我开始编写了一个多条件查询的方法,所以我直接用的是多条件查询,也可以写一个通过用户名查找信息的方法;
/**
* 多条件查询
*
* @param user
* @return
*/
@SelectProvider(type = UserSqlProvider.class, method = "select")
@Results(
{
@Result(column = "id", property = "id", id = true),
@Result(column = "account", property = "account"),
@Result(column = "password", property = "password"),
@Result(column = "img", property = "img"),
@Result(column = "state", property = "state"),
@Result(column = "role", property = "role", one = @One(select = "com.manager.oa.mapper.rbac.RoleMapper.findRoleById", fetchType = FetchType.LAZY)),
}
)
List<User> findUsersByCondition(User user);
然后是Role角色实体类:
@Data // get set toString equals 等
@AllArgsConstructor // 带参构造
@NoArgsConstructor // 无参构造
public class Role {
private Integer id;
private String name; //名称
private String description;//描述
private String state; // 状态
private List<Permission> permissions; //权限集合
private List<Menu> menus; // 菜单集合
}
RoleMapper中的用到的方法:
/**
* 通过ID查询
*
* @param id
* @return
*/
@Select("select * from rbac_roles where id=#{id}")
@Results({
@Result(column = "id",property = "id",id = true),
@Result(column = "id", property = "permissions",many = @Many(select = "com.manager.oa.mapper.rbac.PermissionMapper.findByRoleId")),
@Result(column = "id", property = "menus",many = @Many(select = "com.manager.oa.mapper.rbac.MenuMapper.findByRoleId")),
})
Role findRoleById(int id);
其次是该查询方法中的查询权限和菜单的方法:
/**
* 通过角色ID查询权限
*
* @param roleId
*/
@Select("SELECT * from rbac_permission p,rbac_role_power r WHERE p.id=r.power and role=#{roleId}")
List<Permission> findByRoleId(int roleId);
/**
* 通过角色ID查询菜单
*
* @param roleId
*/
@Select("SELECT * from rbac_menus m,rbac_role_menu r WHERE m.id=r.menu and role=#{roleId} and state='y'")
List<Menu> findByRoleId(int roleId);
完成上面我们就对前端传过来的账号密码做了验证,但是还有验证码没有验证,所以还需要编写验证码过滤器,该过滤器是放在账号密码过滤器之前的,
也就是OaSecurityConfig中的这句话
// 验证码过滤器,加在了用户密码过滤器之前
http.addFilterBefore(captchaFilter(), UsernamePasswordAuthenticationFilter.class);
所以还需要编写验证码过滤器
CaptchaFilter
package com.manager.oa.filter;
import com.google.code.kaptcha.Constants;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* 验证码过滤器
*
* @Author sms
* @Version V1.0.0
* @Date 2022-08-19
*/
public class CaptchaFilter extends OncePerRequestFilter {
@Autowired
private AuthenticationFailureHandler authenticationFailureHandler;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
if ("/portal/tologin".equals(request.getRequestURI()) && "post".equalsIgnoreCase(request.getMethod())) {
String code = request.getParameter("code");
String captcha = request.getSession().getAttribute(Constants.KAPTCHA_SESSION_KEY).toString();
if (!code.equalsIgnoreCase(captcha)) {
// 认证失败
authenticationFailureHandler.onAuthenticationFailure(request, response, new ValidateException("验证码错误"));
return;
}
}
filterChain.doFilter(request, response);
}
}
验证码错误自定义异常
/**
* 验证码错误异常
*
* @Author sms
* @Version V1.0.0
* @Date 2022-08-20
*/
public class ValidateException extends AuthenticationException {
public ValidateException(String msg, Throwable cause) {
super(msg, cause);
}
public ValidateException(String msg) {
super(msg);
}
}
这里的验证码用的是kaptcha
产生验证码的配置类:
package com.manager.oa.config;
import com.google.code.kaptcha.impl.DefaultKaptcha;
import com.google.code.kaptcha.util.Config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.Properties;
import static com.google.code.kaptcha.Constants.*;
/**
* 图片验证码属性配置类
*
* @Author sms
* @Version V1.0.0
* @Date 2022-08-19
*/
@Configuration
public class CaptchaConfig {
@Bean
public DefaultKaptcha defaultKaptcha() {
Properties properties = new Properties();
// 是否有边框 默认为true 我们可以自己设置yes,no
properties.setProperty("kaptcha.border", "yes");
// 验证码文本字符颜色 默认为Color.BLACK
properties.setProperty(KAPTCHA_TEXTPRODUCER_FONT_COLOR, "blue");
// 验证码图片宽度 默认为200
properties.setProperty(KAPTCHA_IMAGE_WIDTH, "100");
// 验证码图片高度 默认为50
properties.setProperty(KAPTCHA_IMAGE_HEIGHT, "34");
// 验证码文本字符大小 默认为40
properties.setProperty(KAPTCHA_TEXTPRODUCER_FONT_SIZE, "30");
// KAPTCHA_SESSION_KEY
properties.setProperty(KAPTCHA_SESSION_CONFIG_KEY, "captcha");
// 验证码文本字符长度 默认为5
properties.setProperty(KAPTCHA_TEXTPRODUCER_CHAR_LENGTH, "4");
// 验证码文本字体样式 默认为new Font("Arial", 1, fontSize), new Font("Courier", 1, fontSize)
properties.setProperty(KAPTCHA_TEXTPRODUCER_FONT_NAMES, "Arial");
// 图片样式 水纹com.google.code.kaptcha.impl.WaterRipple 鱼眼com.google.code.kaptcha.impl.FishEyeGimpy 阴影com.google.code.kaptcha.impl.ShadowGimpy
properties.setProperty(KAPTCHA_OBSCURIFICATOR_IMPL, "com.google.code.kaptcha.impl.WaterRipple");
Config config = new Config(properties);
DefaultKaptcha defaultKaptcha = new DefaultKaptcha();
defaultKaptcha.setConfig(config);
return defaultKaptcha;
}
}
编写验证码的CaptchaController
package com.manager.oa.controller.portal;
import com.google.code.kaptcha.Constants;
import com.google.code.kaptcha.impl.DefaultKaptcha;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import javax.imageio.ImageIO;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.awt.image.BufferedImage;
import java.io.IOException;
/**
* 图片验证码
*
* @Author sms
* @Version V1.0.0
* @Date 2022-08-19
*/
@Controller
public class CaptchaController {
@Autowired
private DefaultKaptcha defaultKaptcha;
@GetMapping(value = "/captcha")
public void getKaptchaImage(HttpServletRequest request, HttpServletResponse response) {
ServletOutputStream out = null;
try {
response.setDateHeader("Expires", 0);
response.setHeader("Cache-Control", "no-store, no-cache, must-revalidate");
response.addHeader("Cache-Control", "post-check=0, pre-check=0");
response.setHeader("Pragma", "no-cache");
response.setContentType("image/jpeg");
//生成验证码
String capText = defaultKaptcha.createText();
HttpSession session = request.getSession();
session.setAttribute(Constants.KAPTCHA_SESSION_KEY, capText);
//向客户端写出
BufferedImage bi = defaultKaptcha.createImage(capText);
out = response.getOutputStream();
ImageIO.write(bi, "jpg", out);
out.flush();
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
if (out != null) {
out.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
最后:LoginController
@Controller("portalLoginController")
@RequestMapping("/portal")
public class LoginController {
@RequestMapping("/login")
public String login() {
return "login";
}
}
最后,我的登录界面
<!DOCTYPE html>
<html lang="zh-cn" xmlns:th="http://www.thymeleaf.org" xmlns:ch="http://www.thymeleaf.org">
<head>
<title>登录</title>
<link rel="stylesheet" th:href="@{/css/pintuer.css}">
<link rel="stylesheet" th:href="@{/css/admin.css}">
<script type="text/javascript">
</script>
</head>
<body>
<div class="bg"></div>
<div class="container">
<div class="line bouncein">
<div class="xs6 xm4 xs3-move xm4-move">
<div style="height:150px;"></div>
<div class="media media-y margin-big-bottom"></div>
<form action="/portal/tologin" method="post">
<div class="panel loginbox">
<div class="text-center margin-big padding-big-top">
<h1>后台管理中心</h1>
<ch:block ch:text="${msg}"></ch:block>
</div>
<div class="panel-body"
style="padding:30px; padding-bottom:10px; padding-top:10px;">
<div class="form-group">
<div class="field field-icon-right">
<!-- <input type="hidden" name="_csrf" th:value="${_csrf.getToken()}">-->
<input type="text" class="input input-big" name="account"
placeholder="登录账号" value="admin3"/> <span
class="icon icon-user margin-small"></span>
</div>
</div>
<div class="form-group">
<div class="field field-icon-right">
<input type="password" class="input input-big" name="password"
placeholder="登录密码" value="123123" /> <span
class="icon icon-key margin-small"></span>
</div>
</div>
<div class="form-group">
<div class="field">
<input type="text" class="input input-big" name="code"
placeholder="填写右侧的验证码" style="width: 225px;display: inline"/>
<img src="/captcha"
alt="" width="100" height="44" onclick="this.src = '/captcha?t=' + new Date().getTime();">
</div>
</div>
<div style="padding:30px;">
<input type="submit"
class="button button-block bg-main text-big input-big"
value="登录">
</div>
</div>
</form>
</div>
</div>
</div>
</body>
</html>
最后需要用到的测试类,及html
权限验证
@PreAuthorize("hasAuthority('dept:list')") // 需要的权限
这个就是最开始从数据库中查询到的权限
@Controller("portalDeptController")
@RequestMapping("/dept")
public class DeptController {
@PreAuthorize("hasAuthority('dept:list')") // 需要的权限
@GetMapping("/list")
public String list(){
return "dept_list";
}
}
NoPermController 权限不足,会话过期controller
package com.manager.oa.controller.portal;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
/**
* @Author sms
* @Version V1.0.0
* @Date 2022-08-22
*/
@Controller("/portal")
public class NoPermController {
/**
* 没有权限
*
* @return
*/
@RequestMapping("/noperm")
public String noPerm() {
return "noperm";
}
/**
* 会话过期
*
* @return
*/
@RequestMapping("/nosession")
public String noSession() {
return "nosession";
}
}
noperm.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
权限不足
</body>
</html>
nosession.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<a href="/portal/login" target="_top">登录</a>
</body>
</html>