上面是hr表,这将代表我们登录时候的用户
上面是role表,代表我们的用户权限
上面是menu_role表,把我门的menu和role联系起来
按照springSecurity的流程,我们先实现userDetailsService,覆写里面的用户认证方法
@Service
public class HrServiceImpl extends ServiceImpl implements HrService, UserDetailsService {
@Autowired
private HrMapper hrMapper;
//验证数据库中是否有这个员工
@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
QueryWrapper wrapper = new QueryWrapper();
wrapper.eq("username",s);
Hr hr = hrMapper.selectOne(wrapper);
if(hr==null){
throw new UsernameNotFoundException("用户名不存在");
}
hr.setRoles(hrMapper.getRoles(s));
return hr;
}
}
再到我们的hr实体类中,它继承了userDetails接口,在这里我们把我们定义权限信息交给springSecurity
//登录员工的类
@Data
@TableName("hr")
public class Hr implements UserDetails {
private int id;
private String name;
private String phone;
private String telephone;
private String address;
private boolean enabled;
private String username;
private String password;
private String remark;
private String userface;
@TableField(exist = false)
private List roles;
//我们定义的role交给springSecurity
@Override
public Collection extends GrantedAuthority> getAuthorities() {
List authorities = new ArrayList<>();
for (String role : roles){
authorities.add(new SimpleGrantedAuthority(role));
}
return authorities;
}
//是否 没过期?
@Override
public boolean isAccountNonExpired() {
return true;
}
//是否 没锁定?
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
//是否 可用?
@Override
public boolean isEnabled(){
return true;
}
}
OK,到这里,我们springSecurity的底层验证逻辑就写好了(通过数据库)。
接下来,就要自定义实现登录过滤器了LoginFilter
public class LoginFilter extends UsernamePasswordAuthenticationFilter {
@Autowired
SessionRegistry sessionRegistry;
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
if (!request.getMethod().equals("POST")) {
throw new AuthenticationServiceException(
"Authentication method not supported: " + request.getMethod());
}
//拿到session中的验证码
String verify_code = (String) request.getSession().getAttribute("verifyCode");
//MediaType是主要告诉服务器request的资源类型
if (request.getContentType().contains(MediaType.APPLICATION_JSON_VALUE) || request.getContentType().contains(MediaType.APPLICATION_JSON_UTF8_VALUE)) {
Map loginData = new HashMap<>();
try {
//ObjectMapper是JackJson的重要类,可实现序列化和反序列化,下面是反序列化成Map对象
loginData = new ObjectMapper().readValue(request.getInputStream(), Map.class);
} catch (IOException e) {
}finally {
String code = loginData.get("code");
checkCode(response, code, verify_code);
}
String username = loginData.get(getUsernameParameter());
String password = loginData.get(getPasswordParameter());
if (username == null) {
username = "";
}
if (password == null) {
password = "";
}
username = username.trim();
//进行用户名和密码的验证
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
username, password);
setDetails(request, authRequest);
Hr principal = new Hr();
principal.setUsername(username);
sessionRegistry.registerNewSession(request.getSession(true).getId(), principal);
return this.getAuthenticationManager().authenticate(authRequest);
} else {
checkCode(response, request.getParameter("code"), verify_code);
return super.attemptAuthentication(request, response);
}
}
public void checkCode(HttpServletResponse resp, String code, String verify_code) {
if (code == null || verify_code == null || "".equals(code) || !verify_code.toLowerCase().equals(code.toLowerCase())) {
//验证码不正确
throw new AuthenticationServiceException("验证码不正确");
}
}
}
这个过滤器,会将我们前端表单登录的信息,进行底层逻辑验证(用户名和密码),我们在这里面自己添加了一个校验验证码是否正确的方法。这里面还用到了JackJson,进行反序列化。相关依赖自己添加。
完成了LoginFilter后,我们就要实现根据当前url判断我现在的hr是否有权限能访问相关的资源,这里面我们覆写springsecurity的
FilterInvocationSecurityMetadataSource
@Component
public class MyFilterInvocationSecurityMetadataSource implements FilterInvocationSecurityMetadataSource {
@Autowired
MenuServiceImpl menuService;
@Autowired
MenuMapper menuMapper;
AntPathMatcher antPathMatcher = new AntPathMatcher();
@Override
public Collection getAttributes(Object o) throws IllegalArgumentException {
String requestUrl = ((FilterInvocation) o).getRequestUrl();
List
完成好了配置,接下来就要教springSecurity跟我们定义好的资源来放行或者拦截
@Component
public class UrlDecisionManager implements AccessDecisionManager {
@Override
public void decide(Authentication authentication, Object o, Collection collection) throws AccessDeniedException, InsufficientAuthenticationException {
for (ConfigAttribute configAttribute : collection) {
String needRole = configAttribute.getAttribute();
if ("ROLE_LOGIN".equals(needRole)) {
if (authentication instanceof AnonymousAuthenticationToken) {
throw new AccessDeniedException("尚未登录,请登录!");
}else {
return;
}
}
Collection extends GrantedAuthority> authorities = authentication.getAuthorities();
for (GrantedAuthority authority : authorities) {
if (authority.getAuthority().equals(needRole)) {
return;
}
}
}
throw new AccessDeniedException("权限不足,请联系管理员!");
}
@Override
public boolean supports(ConfigAttribute configAttribute) {
return true;
}
@Override
public boolean supports(Class> aClass) {
return true;
}
}
最后我们要到springSecurity统一的配置中心。相当于把我们自己定制好的SpringSecurity交给SpringBoot来管辖。
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
HrServiceImpl hrService;
@Autowired
MyFilterInvocationSecurityMetadataSource myFilterInvocationSecurityMetadataSource;
@Autowired
UrlDecisionManager urlDecisionManager;
@Bean
SessionRegistryImpl sessionRegistry() {
return new SessionRegistryImpl();
}
//配置密码的编码格式
@Bean
PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
//不需要过滤的地址
@Override
public void configure(WebSecurity web){
web.ignoring().antMatchers("/css/**", "/js/**", "/index.html", "/img/**", "/fonts/**", "/favicon.ico","/verifyCode");
}
//将我们的认证功能交给spring管理
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
//配置认证的实现方式
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(hrService).passwordEncoder(passwordEncoder());
}
@Bean
LoginFilter loginFilter() throws Exception {
LoginFilter loginFilter = new LoginFilter();
loginFilter.setAuthenticationSuccessHandler((request, response, authentication) -> {
response.setContentType("application/json;charset=utf-8");
PrintWriter out = response.getWriter();
Hr hr = (Hr) authentication.getPrincipal();
hr.setPassword(null);
RespBean ok = new RespBean(200,"登录成功!", hr);
String s = new ObjectMapper().writeValueAsString(ok);
out.write(s);
out.flush();
out.close();
}
);
loginFilter.setAuthenticationFailureHandler((request, response, exception) -> {
response.setContentType("application/json;charset=utf-8");
PrintWriter out = response.getWriter();
RespBean respBean = new RespBean(500,exception.getMessage(),null);
if (exception instanceof LockedException) {
respBean.setMsg("账户被锁定,请联系管理员!");
} else if (exception instanceof CredentialsExpiredException) {
respBean.setMsg("密码过期,请联系管理员!");
} else if (exception instanceof AccountExpiredException) {
respBean.setMsg("账户过期,请联系管理员!");
} else if (exception instanceof DisabledException) {
respBean.setMsg("账户被禁用,请联系管理员!");
} else if (exception instanceof BadCredentialsException) {
respBean.setMsg("用户名或者密码输入错误,请重新输入!");
}
out.write(new ObjectMapper().writeValueAsString(respBean));
out.flush();
out.close();
}
);
loginFilter.setAuthenticationManager(authenticationManagerBean());
loginFilter.setFilterProcessesUrl("/doLogin");
ConcurrentSessionControlAuthenticationStrategy sessionStrategy = new ConcurrentSessionControlAuthenticationStrategy(sessionRegistry());
//用户登录的并发控制,只允许用户单点登录
sessionStrategy.setMaximumSessions(1);
loginFilter.setSessionAuthenticationStrategy(sessionStrategy);
return loginFilter;
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.withObjectPostProcessor(new ObjectPostProcessor() {
@Override
public O postProcess(O object) {
object.setAccessDecisionManager(urlDecisionManager);
object.setSecurityMetadataSource(myFilterInvocationSecurityMetadataSource);
return object;
}
})
.and()
.logout()
.logoutSuccessHandler((req, resp, authentication) -> {
resp.setContentType("application/json;charset=utf-8");
PrintWriter out = resp.getWriter();
out.write(new ObjectMapper().writeValueAsString(new RespBean(200,"注销成功!",null)));
out.flush();
out.close();
}
)
.permitAll()
.and()
.csrf().disable().exceptionHandling()
//没有认证时,在这里处理结果,不要重定向
.authenticationEntryPoint((req, resp, authException) -> {
resp.setContentType("application/json;charset=utf-8");
resp.setStatus(401);
PrintWriter out = resp.getWriter();
RespBean respBean = new RespBean(500,"访问失败!",null);
if (authException instanceof InsufficientAuthenticationException) {
respBean.setMsg("请求失败,请联系管理员!");
}
out.write(new ObjectMapper().writeValueAsString(respBean));
out.flush();
out.close();
}
);
http.addFilterAt(new ConcurrentSessionFilter(sessionRegistry(), event -> {
HttpServletResponse resp = event.getResponse();
resp.setContentType("application/json;charset=utf-8");
resp.setStatus(401);
PrintWriter out = resp.getWriter();
out.write(new ObjectMapper().writeValueAsString(new RespBean(400,"您已在另一台设备登录,本次登录已下线!",null) ));
out.flush();
out.close();
}), ConcurrentSessionFilter.class);
http.addFilterAt(loginFilter(), UsernamePasswordAuthenticationFilter.class);
}
}
至此,我们完成了整个自定义SpringSecurity的认证和授权体系。
对了,下面是自己写的验证码生成。
@Component
public class VerificationCode {
//设置验证码图片的宽、高
private final int width = 100;
private final int height = 30;
private Image image;
private String[] fontNames = new String[]{"微软雅黑","宋体","楷体","黑体"};
//设置验证码的内容范围
private String codes = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
//验证码的内容
private String text;
/*
* 设置随机code的颜色
* */
public Color getColor(){
int Red = new Random().nextInt(150);
int Blue = new Random().nextInt(150);
int Green = new Random().nextInt(150);
return new Color(Red,Green,Blue);
}
/*
* 设置随机font的种类
* */
public Font getFont(){
String name = fontNames[ new Random().nextInt(4) ];
int style = new Random().nextInt(4);
int size = new Random().nextInt(5)+24;
return new Font(name,style,size);
}
/*
* 创建一个验证码图片对象,
* */
public BufferedImage getImage(){
//创建一个BufferImage,它可以在内存中操作
BufferedImage image = new BufferedImage(width,height,BufferedImage.TYPE_INT_BGR);
Graphics2D g2D = (Graphics2D) image.getGraphics();
g2D.fillRect(0,0,width,height);
//设置验证码的背景图案为白色
g2D.setBackground(Color.white);
StringBuilder text = new StringBuilder();
//生成4个code形成验证码内容
for(int i=0;i<4;i++){
char r = codes.charAt( new Random().nextInt(62) );
text.append(r);
g2D.setColor( getColor() );
g2D.setFont( getFont() );
g2D.drawString(String.valueOf(r),i*25,height-8);
}
this.text = text.toString();
return image;
}
/*
*
* */
public String getText(){
return this.text;
}
}
@RestController
public class LoginController {
@GetMapping("/verifyCode")
public void getCode(HttpServletRequest request, HttpServletResponse response) throws IOException {
VerificationCode vfCode = new VerificationCode();
BufferedImage image = vfCode.getImage();
String text = vfCode.getText();
HttpSession session = request.getSession();
session.setAttribute("verifyCode",text);
ImageIO.write(image, "JPEG",response.getOutputStream() );
}
}
根据地址栏,可以很清楚的看到我们已经由(/login)跳转到/home来了。但是我还没有写关于/home的资源controller。所以看不到界面。但是这篇文章主要是写SpringSecutiry的。