-
创建springboot工程,和引入springSecurity依赖、lombok依赖,mysql依赖,myBatisPlus依赖等相关依赖(根据自己所需)。
但这不是我们的重点,这些相关配置自己实现创建就行,这里就不列出来。 -
先看一下我的数据库里面的配置信息
上面是hr表,这将代表我们登录时候的用户
上面是role表,代表我们的用户权限上面是hr_role表,把我们的hr和role联系起来
这是menu表,代表我们请求url需要用户的什么权限
上面是menu_role表,把我门的menu和role联系起来
-
按照springSecurity的流程,我们先实现userDetailsService,覆写里面的用户认证方法
@Service public class HrServiceImpl extends ServiceImpl<HrMapper, Hr> 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<String> roles; //我们定义的role交给springSecurity @Override public Collection<? extends GrantedAuthority> getAuthorities() { List<SimpleGrantedAuthority> 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<String, String> 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<ConfigAttribute> getAttributes(Object o) throws IllegalArgumentException { String requestUrl = ((FilterInvocation) o).getRequestUrl(); List<Menu> menus = menuService.list(); for (Menu menu : menus) { if (antPathMatcher.match(menu.getUrl(), requestUrl)) { List<String> roles = menuMapper.getRoles(menu.getId()); String[] str = new String[roles.size()]; for (int i = 0; i < roles.size(); i++) { str[i] = roles.get(i); } return SecurityConfig.createList(str); } } return SecurityConfig.createList("ROLE_LOGIN"); } @Override public Collection<ConfigAttribute> getAllConfigAttributes() { return null; } @Override public boolean supports(Class<?> aClass) { return true; } }
-
完成好了配置,接下来就要教springSecurity跟我们定义好的资源来放行或者拦截
@Component public class UrlDecisionManager implements AccessDecisionManager { @Override public void decide(Authentication authentication, Object o, Collection<ConfigAttribute> 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<FilterSecurityInterceptor>() { @Override public <O extends FilterSecurityInterceptor> 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的。
暂无评论...