原来大多数单体项目都是用的shiro,随着分布式的逐渐普及以及与Spring的天生天然的结合。Spring Security安全框架越受你们的青睐。本文会教你用SpringSecurity设计单项目的权限,关于如何作分布式的权限,后续会跟进。
现现在,在JavaWeb的世界里Spring能够说是一统江湖,随着微服务的到来,SpringCloud能够说是Java程序员必须熟悉的框架,就连阿里都为SpringCloud写开源呢。(好比大名鼎鼎的Nacos)做为Spring的亲儿子,SpringSecurity很好的适应了了微服务的生态。你能够很是简便的结合Oauth作认证中心服务。本文先从最简单的单体项目开始,逐步掌握Security。更多可达官方文档html
我准备了一个简单的demo,具体代码会放到文末。提早声明,本demo没有用JWT,由于我想把token的维护放到服务端,更好的维护过时时间。(固然,若是未来微服务认证中心的形式,JWT也能够作到方便的维护过时时间,不作过多讨论)若是想了解Security+JWT简易入门,请戳前端
本项目结构以下git
另外,本demo使用了MybatisPlus、lombok。程序员
首先须要实现两个类,一个是UserDetails的实现类SecurityUser,一个是UserDetailsService的实现类SecurityUserService。github
** * Security 要求须要实现的User类 * */ @Data public class SecurityUser implements UserDetails { @Autowired private SysRoleService sysRoleService; //用户登陆名(注意此处的username和SysUser的loginName是一个值) private String username; //登陆密码 private String password; //用户id private SysUser sysUser; //该用户的全部权限 private List<SysMenu> sysMenuList; /**构造函数*/ public SecurityUser(SysUser sysUser){ this.username = sysUser.getLoginName(); this.password = sysUser.getPassword(); this.sysUser = sysUser; } public SecurityUser(SysUser sysUser,List<SysMenu> sysMenuList){ this.username = sysUser.getLoginName(); this.password = sysUser.getPassword(); this.sysMenuList = sysMenuList; this.sysUser = sysUser; } /**须要实现的方法*/ @Override public Collection<? extends GrantedAuthority> getAuthorities() { List<GrantedAuthority> authorities = new ArrayList<>(); for(SysMenu menu : sysMenuList) { authorities.add(new SimpleGrantedAuthority(menu.getPerms())); } return authorities; } @Override public String getPassword() { return this.password; } @Override public String getUsername() { return this.username; } //默认帐户未过时 @Override public boolean isAccountNonExpired() { return true; } //默认帐户没有带锁 @Override public boolean isAccountNonLocked() { return true; } //默认凭证没有过时 @Override public boolean isCredentialsNonExpired() { return true; } //默认帐户可用 @Override public boolean isEnabled() { return true; } }
这个类包含着某个请求者的信息,在Security中叫作主体。其中这个方法是必须实现的,能够获取用户的具体权限。咱们这边权限的颗粒度达到了菜单级别,而不是不少开源项目中角色那级别,我以为颗粒度越细越方便(我的以为...)spring
/** * Security 要求须要实现的UserService类 * */ @Service public class SecurityUserService implements UserDetailsService{ @Autowired private SysUserService sysUserService; @Autowired private SysMenuService sysMenuService; @Autowired private HttpServletRequest httpServletRequest; @Override public SecurityUser loadUserByUsername(String loginName) throws UsernameNotFoundException { LambdaQueryWrapper<SysUser> condition = Wrappers.<SysUser>lambdaQuery().eq(SysUser::getLoginName, loginName); SysUser sysUser = sysUserService.getOne(condition); if (Objects.isNull(sysUser)){ throw new UsernameNotFoundException("未找到该用户!"); } Long projectId = null; try{ projectId = Long.parseLong(httpServletRequest.getHeader("projectId")); }catch (Exception e){ } SysMenuModel sysMenuModel; if (sysUser.getUserType()){ sysMenuModel = new SysMenuModel(); }else { sysMenuModel = new SysMenuModel().setUserId(sysUser.getId()); } sysMenuModel.setProjectId(projectId); List<SysMenu> menuList = sysMenuService.getList(sysMenuModel); return new SecurityUser(sysUser,menuList); } }
显而易见,这个类实现了惟一的方法loadUserByUsername,从而能够拿到某用户的全部权限,并生成主体,在后面的filter中就能够见到他的做用了。数据库
在看配置和filter以前,还有一个类须要说明一下,此类提供方法,可让用户未登陆、或者token失效的状况下进行统一返回。后端
@Component public class SecurityAuthenticationEntryPoint implements AuthenticationEntryPoint, Serializable { private static final long serialVersionUID = 1L; @Override public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException { response.sendError(HttpServletResponse.SC_UNAUTHORIZED,"token失效,请登录后重试"); } }
ok,接下来看配置,实现了WebSecurityConfigurerAdapter的SecurityConfig类,特别说明,本demo算是先后端分离的前提下写的,因此实现过多的方法,其实这个类能够实现三个方法,具体请戳。缓存
@Configuration @EnableWebSecurity @EnableGlobalMethodSecurity(prePostEnabled = true) public class SecurityConfig extends WebSecurityConfigurerAdapter{ @Autowired SecurityAuthenticationEntryPoint securityAuthenticationEntryPoint; @Autowired SecurityFilter securityFilter; @Override protected void configure(HttpSecurity http) throws Exception { http //禁止csrf .csrf().disable() //异常处理 .exceptionHandling().authenticationEntryPoint(securityAuthenticationEntryPoint).and() //Session管理方式 .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and() //开启认证 .authorizeRequests() .antMatchers("/login/login").permitAll() .antMatchers("/login/register").permitAll() .antMatchers("/login/logout").permitAll() .anyRequest().authenticated(); http .addFilterBefore(securityFilter, UsernamePasswordAuthenticationFilter.class); } }
异常处理就是上面那个类,Session那几种管理方式我在那篇Security+JWT的文章中也有所讲解,比较简单,而后是几个不用验证的登陆路径,剩下的都须要通过咱们下面这个filter。安全
@Slf4j @Component public class SecurityFilter extends OncePerRequestFilter { @Autowired SecurityUserService securityUserService; @Autowired SysUserService sysUserService; @Autowired SysUserTokenService sysUserTokenService; /** * 认证受权 * */ @Override protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException { log.info("访问的连接是:{}",httpServletRequest.getRequestURL()); try { final String token = httpServletRequest.getHeader("token"); LambdaQueryWrapper<SysUserToken> condition = Wrappers.<SysUserToken>lambdaQuery().eq(SysUserToken::getToken, token); SysUserToken sysUserToken = sysUserTokenService.getOne(condition); if (Objects.nonNull(sysUserToken)){ SysUser sysUser = sysUserService.getById(sysUserToken.getUserId()); if (Objects.nonNull(sysUser)){ SecurityUser securityUser = securityUserService.loadUserByUsername(sysUser.getLoginName()); //将主体放入内存 UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(securityUser, null, securityUser.getAuthorities()); authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(httpServletRequest)); //放入内存中去 SecurityContextHolder.getContext().setAuthentication(authentication); } } }catch (Exception e){ log.error("认证受权时出错:{}", Arrays.toString(e.getStackTrace())); } filterChain.doFilter(httpServletRequest, httpServletResponse); } }
判断用户是否登陆,就是从数据库中查看是否有未过时的token,若是存在,就把主体信息放进到项目的内存中去,特别说明的是,每一个请求链结束,SecurityContextHolder.getContext()的数据都会被clear的,因此,每次请求的时候都须要set。
以上就完成了Security核心的建立,为了业务代码方便获取内存中的主体信息,我特地加了一个获取用户信息的方法
/** * 获取Security主体工具类 * @author pjjlt * */ public class SecurityUserUtil { public static SysUser getCurrentUser(){ SecurityUser securityUser = (SecurityUser) SecurityContextHolder.getContext().getAuthentication().getPrincipal(); if (Objects.nonNull(securityUser) && Objects.nonNull(securityUser.getSysUser())){ return securityUser.getSysUser(); } return null; } }
以上是Security核心代码,下面简单加两个业务代码,好比登陆和某个接口的权限访问测试。
首先,不被filter拦截的那三个方法注册、登陆、登出,我都写在了moudle.controller.LoginController这个路径下,注册就不用说了,就是一个insertUser的方法,作好判断就好,密码经过AES加个密。
下面看下登陆代码,controller层就不说了,反正就是个验参。
/** * 登陆,返回登陆信息,前端须要缓存 * */ @Override @Transactional(rollbackFor = Exception.class) public JSONObject login(SysUserModel sysUserModel) throws Exception{ JSONObject result = new JSONObject(); //1. 验证帐号是否存在、密码是否正确、帐号是否停用 Wrapper<SysUser> sysUserWrapper = Wrappers.<SysUser>lambdaQuery() .eq(SysUser::getLoginName,sysUserModel.getLoginName()).or() .eq(SysUser::getEmail,sysUserModel.getEmail()); SysUser sysUser = baseMapper.selectOne(sysUserWrapper); if (Objects.isNull(sysUser)){ throw new Exception("用户不存在!"); } String password = CipherUtil.encryptByAES(sysUserModel.getPassword()); if (!password.equals(sysUser.getPassword())){ throw new Exception("密码不正确!"); } if (sysUser.getStatus()){ throw new Exception("帐号已删除或已停用!"); } // 2.更新最后登陆时间 sysUser.setLoginIp(ServletUtil.getClientIP(request)); sysUser.setLoginDate(LocalDateTime.now()); baseMapper.updateById(sysUser); // 3.封装token,返回信息 String token = UUID.fastUUID().toString().replace("-",""); LocalDateTime expireTime = LocalDateTime.now().plusSeconds(expireTimeSeconds); SysUserToken sysUserToken = new SysUserToken() .setToken(token).setUserId(sysUser.getId()).setExpireTime(expireTime); sysUserTokenService.save(sysUserToken); result.putOpt("token",token); result.putOpt("expireTime",expireTime); return result; }
首先验证下用户是否存在,登陆密码是否正确,而后封装token,值得一提的是,我并无从数据库(sysUserToken)中获取用户已经登陆的token,而后更新过时时间的形式作登陆,而是每次登陆都获取新token,这样就能够作到多端登陆了,后期还能够作帐号登陆数量的控制。
而后就是登出,删除库中存在的token
/** * 登出,删除token * */ @Override public void logout() throws Exception{ String token = httpServletRequest.getHeader("token"); if (Objects.isNull(token)){ throw new LoginException("token不存在",ResultEnum.LOGOUT_ERROR); } LambdaQueryWrapper<SysUserToken> sysUserWrapper = Wrappers.<SysUserToken>lambdaQuery() .eq(SysUserToken::getToken,token); baseMapper.delete(sysUserWrapper); }
这边我维护了两个帐号,一个是超级管理员majian,拥有全部权限。一个是普通人员_pjjlt,只有一些权限,咱们看一下访问接口的效果。
咱们访问的接口是moudle.controller.LoginController路径下的
@PreAuthorize("hasAnyAuthority('test')") @GetMapping("test") public String test(){ return "test"; }
其中hasAnyAuthority('test')就是权限码
咱们模拟用不一样帐号访问,就是改变请求header中的token值,就是登陆阶段返回给前端的token。
首先是超级管理员验证
而后是普通管理员访问
接着没有登陆(token不存在或者已过时)访问
https://github.com/majian1994...
本文简单讲解了,主要是将Security相关的东西,具体实现角色的三要素,用户、角色、权限(菜单)能够看个人代码,都写完测完了,原本想写个文档管理系统,帮助我司更好的管理接口文档,but有位小伙伴找了一个不错的开源的了,因此这代码就成了个人一个小demo。 最后的最后,可不能够放波公众号啊,之后打算公众号同步写文章。