之前项目都用的是 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 下 - 动态角色和权限验证 中会讲解如何动态增加角色和权限验证。