SpringBoot & SpringSecurity 上 - 静态角色
之前项目都用的是 shiro 这次改用 SpringSecurity,特意记录一下。
一、添加 WebSecurityConfig
代码如下,这个文件是 SpringSecurity 配置主入口
package com.bjy.qa.util.security; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.authentication.AuthenticationManager; 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.config.http.SessionCreationPolicy; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import javax.annotation.Resource; /** * SpringSecurity 配置 */ @Configuration @EnableWebSecurity @EnableGlobalMethodSecurity(prePostEnabled = true) // 开启方法级别的权限认证,后续不需要,要删除 public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Resource private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter; // JWT 过滤器 @Resource AuthenticationEntryPointImpl autoetaticaticcAutryPointImpl; // 认证 时的异常(当用户请求一个受保护的资源,又没登录时触发) @Resource AccessDeniedPointImpl accessDeniedPointImpl; // 用户访问无权限资源 时的异常(用户登录后,请求一个受保护的资源,又没权限时触发) @Override protected void configure(HttpSecurity http) throws Exception { http .csrf().disable() // 关闭跨站请求防护 .cors().and() // 配置 CORS支持 .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) // 不通过 session 获取 SecurityContext .and().authorizeRequests().antMatchers("/user/user/login").anonymous() // 对于登录接口允许匿名访问 .anyRequest().authenticated(); // 除上面外的所有请求全部需要鉴权认 // 配置异常处理器 http .exceptionHandling() .authenticationEntryPoint(autoetaticaticcAutryPointImpl) // 认证 时的异常(当用户请求一个受保护的资源,又没登录时触发) .accessDeniedHandler(accessDeniedPointImpl); // 用户访问无权限资源 时的异常(用户登录后,请求一个受保护的资源,又没权限时触发) // 添加 JWT 过滤器 http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class); } @Bean @Override public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); } }
二、LoginUser
这个文件实现了 SpringSecurity 的 UserDetails,有两个作用一个是后续会把此对象缓存到 redis 中,另一个作用的会存入 SpringSecurity 的 context 中。
package com.bjy.qa.util.security; import com.alibaba.fastjson.annotation.JSONField; import com.bjy.qa.entity.user.User; import lombok.Data; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.userdetails.UserDetails; import java.util.Collection; import java.util.List; import java.util.stream.Collectors; @Data public class LoginUser implements UserDetails { private User user; // 用户对象 private List<String> permissions; // 权限列表(数据库中保存的 list) @JSONField(serialize = false) private List<SimpleGrantedAuthority> authorities; // Spring Security 中用到的权限列表(SimpleGrantedAuthority 类型) public LoginUser(User user, List<String> permissions) { this.user = user; this.permissions = permissions; } /** * 返回当前用户所拥有的权限信息 * @return */ @Override public Collection<? extends GrantedAuthority> getAuthorities() { // 如果已经转过,直接返回 if (authorities != null) { return authorities; } // 把数据库中的权限列表(permissions)转为 Spring Security 中用到的权限列表(authorities)。String 转 SimpleGrantedAuthority authorities = permissions.stream().map(SimpleGrantedAuthority::new).collect(Collectors.toList()); return authorities; } /** * 获取密码 * @return */ @Override public String getPassword() { return "{noop}" + user.getCode(); } /** * 获取账号名称 * @return */ @Override public String getUsername() { return user.getAccount(); } /** * 账号是否过期 * @return true:未过期;false:已过期 */ @Override public boolean isAccountNonExpired() { return true; } /** * 账号是否被锁定 * @return true:未锁定;false:已锁定 */ @Override public boolean isAccountNonLocked() { return !user.isLocked(); } /** * 密码是否过期 * @return true:未过期;false:已过期 */ @Override public boolean isCredentialsNonExpired() { return true; } /** * 是否可用 * @return treu:可用;false:不可用 */ @Override public boolean isEnabled() { return true; } }
三、JwtAuthenticationTokenFilter
jwt 过滤器,扩展了 SpringSecurity 的 OncePerRequestFilter,每次请求后会调用此过滤器,其根据 token 中的 userId 从 redis 中拿回 LoninUser 信息。
四、CustomUserDetailsService
用户详细信息,实现了 UserDetailsService 接口,登录时会调用此类查询用户数据。
package com.bjy.qa.util.security; import com.bjy.qa.dao.user.UserDao; import com.bjy.qa.entity.user.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.Service; import javax.annotation.Resource; import java.util.ArrayList; import java.util.Arrays; import java.util.List; /** * 用户详细信息 */ @Service public class CustomUserDetailsService implements UserDetailsService { @Resource UserDao userDao; @Override public UserDetails loadUserByUsername(String userName) throws UsernameNotFoundException { User user = userDao.selectOne(userName); if (user == null) { throw new RuntimeException("登录失败,用户名或密码错误!"); } // todo: 从数据库中获取用户权限信息 List<String> list = new ArrayList<>(Arrays.asList("ROLE_ADMIN", "ROLE_USER", "admin")); return new LoginUser(user, list); } }
五、AuthenticationEntryPointImpl
认证 时的异常(当用户请求一个受保护的资源,又没登录时触发),实现了 AuthenticationEntryPoint 接口。当认证(登录)失败回调此方法。
package com.bjy.qa.util.security; import com.alibaba.fastjson.JSONObject; import com.alibaba.fastjson.parser.ParserConfig; import com.bjy.qa.entity.Response; import com.bjy.qa.enumtype.ErrorCode; import org.springframework.security.core.AuthenticationException; import org.springframework.security.web.AuthenticationEntryPoint; import org.springframework.stereotype.Component; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; /** * 认证 时的异常(当用户请求一个受保护的资源,又没登录时触发) */ @Component public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint { @Override public void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException { ParserConfig.getGlobalInstance().setAutoTypeSupport(true); httpServletResponse.setCharacterEncoding("UTF-8"); httpServletResponse.setContentType("application/json"); httpServletResponse.getWriter().println(JSONObject.toJSONString(Response.fail(ErrorCode.FORBIDDEN, null))); // 401 httpServletResponse.getWriter().flush(); } }
六、AccessDeniedPointImpl
用户访问无权限资源 时的异常(用户登录后,请求一个受保护的资源,又没权限时触发),实现了 AccessDeniedHandler。当没有权限时回调此方法。
package com.bjy.qa.util.security; import com.alibaba.fastjson.JSONObject; import com.alibaba.fastjson.parser.ParserConfig; import com.bjy.qa.entity.Response; import com.bjy.qa.enumtype.ErrorCode; import org.springframework.security.access.AccessDeniedException; import org.springframework.security.web.access.AccessDeniedHandler; import org.springframework.stereotype.Component; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; /** * 用户访问无权限资源 时的异常(用户登录后,请求一个受保护的资源,又没权限时触发) */ @Component public class AccessDeniedPointImpl implements AccessDeniedHandler { @Override public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AccessDeniedException e) throws IOException, ServletException { ParserConfig.getGlobalInstance().setAutoTypeSupport(true); httpServletResponse.setCharacterEncoding("UTF-8"); httpServletResponse.setContentType("application/json"); httpServletResponse.getWriter().println(JSONObject.toJSONString(Response.fail(ErrorCode.UNAUTHORIZED, null))); // 403 httpServletResponse.getWriter().flush(); } }
七、修改登录(login)接口
@RequestMapping(value = "/login", method = {RequestMethod.POST}) @ResponseBody @Validated(User.UserLoingGroup.class) @ApiOperation(value = "用户登录", notes = "用户登录接口") public Response<UserResponse> login(@RequestBody @Valid User user) { logger.info("{}", user); // 使用 UsernamePasswordAuthenticationToken 认证 UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(user.getAccount(), user.getCode()); Authentication authentication = authenticationManager.authenticate(authenticationToken); // 认证 // 如果认证没通过,给出错误提示 if (authentication == null) { throw new RuntimeException("登录失败,用户名或密码错误!"); } // 如果认证通过,生成 token 返回 LoginUser loginUser = (LoginUser) authentication.getPrincipal(); String userId = loginUser.getUser().getId().toString(); //Response response = Response.success(iUserService.login(user)); Response response = Response.success("token=" + userId); logger.info("{}", response); return response; }
注意 login 接口地址,要跟刚才 WebSecurityConfig 中放开的 anonymous 地址相同
八、随便找一个接口添加访问角色
@PreAuthorize("hasAnyAuthority('admin')") 添加后就要有 admin 角色才能访问
@PreAuthorize("hasAnyAuthority('admin')") @RequestMapping(value = "/logout", method = {RequestMethod.GET, RequestMethod.POST}) @ResponseBody @ApiOperation(value = "用户退出", notes = "用户退出") public String logout() throws Exception { // 获取SecurtiryContextHolder 中获取用户id UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = (UsernamePasswordAuthenticationToken) SecurityContextHolder.getContext().getAuthentication(); LoginUser loginUser = (LoginUser) usernamePasswordAuthenticationToken.getPrincipal(); Long userId = loginUser.getUser().getId(); // redisCache.deleteObject("login:" + userId); // TODO: 2022/6/4 临时写的没有完成资源释放 return "{\"code\":0,\"data\":{}}"; }
注意加的这个角色一定要在 JwtAuthenticationTokenFilter 中存在
九、测试
下图是登录接口测试结果
下图是传入一个错误的 token ,token 解码错误的图
修改成正确的 token 后请求正常
将 logout 接口改一个没有的角色,再请求提示没权限
以上就是 SpringSecurity 的标准用法,每个接口需要提前定义哪些角色可以访问,这样就不能动态增加角色了。SpringBoot & SpringSecurity 下 - 动态角色和权限验证 中会讲解如何动态增加角色和权限验证。
参考文档:
https://www.bilibili.com/video/BV1mm4y1X7Hc?p=31