在管理后台的开发中经常需要对用户授权及权限控制,用户登录后,需要对用户拥有的角色来判断能够访问的资源
统一对登录后的token校验以及注解式的角色权限控制
首选简单创建5张表来存储用户、角色、资源表及用户-角色、角色-资源的关联表
创建一个适配器,来配置哪些接口需要鉴权,鉴权的方式
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true)
public class AuthServerConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserDetailsService userDetailsService;
@Autowired
private StringRedisTemplate redisTemplate;
@Override
protected void configure(HttpSecurity http) throws Exception {
LogoutConfigurer<HttpSecurity> httpSecurityLogoutConfigurer = http.cors().and().csrf().disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
.authorizeRequests()
.antMatchers(AuthWhiteList.AUTH_WHITE_LIST).permitAll()
.anyRequest().authenticated() // 所有请求需要身份认证
.and()
.addFilter(new JWTAuthenticationFilter(authenticationManager(), redisTemplate))
.addFilter(new JWTLoginFilter(authenticationManager()))
.exceptionHandling()
.authenticationEntryPoint(authenticationEntryPoint())// 自定义身份验证入口点
.accessDeniedHandler(accessDeniedHandler()) // 自定义访问失败处理器
.and()
.formLogin()
//.successHandler(authenticationSuccessHandler())// 认证成功处理器
//.failureHandler(authenticationFailureHandler())// 认证失败处理器
.and()
.logout() // 默认注销行为为logout,可以通过下面的方式来修改
.logoutUrl("/logout")
.logoutSuccessUrl("/login")// 设置注销成功后跳转页面,默认是跳转到登录页面;
.permitAll();
}
/**
* 指定加密方式
*/
@Bean
public PasswordEncoder passwordEncoder() {
// 使用BCrypt加密密码
return new MySecurityPasswordEncoder();
}
// 该方法是登录的时候会进入
@Override
public void configure(AuthenticationManagerBuilder auth) throws Exception {
// 使用自定义身份验证组件
auth.authenticationProvider(new CustomAuthenticationProvider(userDetailsService, passwordEncoder()));
}
@Bean
public AuthenticationEntryPoint authenticationEntryPoint() {
return new CustomAuthenticationEntryPoint();
}
@Bean
public AuthenticationSuccessHandler authenticationSuccessHandler() {
return new CustomAuthenticationSuccessHandler();
}
@Bean
public AuthenticationFailureHandler authenticationFailureHandler() {
return new CustomAuthenticationFailureHandler();
}
@Bean
public AccessDeniedHandler accessDeniedHandler() {
return new CustomAccessDeniedHandler();
}
}
自定义认证过滤器
/**
* 自定义JWT认证过滤器
* 该类继承自BasicAuthenticationFilter,在doFilterInternal方法中,
* 从http头的Authorization 项读取token数据,然后用Jwts包提供的方法校验token的合法性。
* 如果校验通过,就认为这是一个取得授权的合法请求
*/
public class JWTAuthenticationFilter extends BasicAuthenticationFilter {
private static final Logger logger = LoggerFactory.getLogger(JWTAuthenticationFilter.class);
private StringRedisTemplate redisTemplate;
public JWTAuthenticationFilter(AuthenticationManager authenticationManager, StringRedisTemplate redisTemplate) {
super(authenticationManager);
this.redisTemplate = redisTemplate;
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
String requestURI = request.getRequestURI();
String header = request.getHeader(ConstantKey.HEADER_KEY);
if (StringUtils.isBlank(header) || !header.startsWith(ConstantKey.BEARER)) {
chain.doFilter(request, response);
return;
}
// 如果token不为空,并且是以指定票据开头
if (StringUtils.isNotBlank(header) && header.startsWith(ConstantKey.BEARER)) {
// 如果请求路径是放行路径,则直接跳过认证
List<String> anonUrlList = Arrays.asList(AuthWhiteList.AUTH_WHITE_LIST);
if (anonUrlList.contains(requestURI)) {
chain.doFilter(request, response);
return;
}
}
UsernamePasswordAuthenticationToken authentication = getAuthentication(request, response);
SecurityContextHolder.getContext().setAuthentication(authentication);
chain.doFilter(request, response);
}
private UsernamePasswordAuthenticationToken getAuthentication(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
try {
long start = System.currentTimeMillis();
String token = request.getHeader(ConstantKey.HEADER_KEY);
if (StringUtils.isBlank(token)) {
throw new ServiceException("Token不能为空!");
}
// parse the token.
Claims claims = JwtUtil.parseJWT(token.replace(ConstantKey.BEARER, ""));
String userId = claims.getSubject();
long end = System.currentTimeMillis();
logger.info("执行时间: {}", (end - start) + " 毫秒");
if (StringUtils.isNotBlank(userId)) {
List<GrantedAuthority> authorities = new ArrayList<>();
String roleStr = redisTemplate.opsForValue().get("user_role_" + userId);
if (StringUtils.isNotBlank(roleStr)) {
List<String> authoritieStr = JSON.parseArray(roleStr, String.class);
for (String str : authoritieStr) {
authorities.add(new SimpleGrantedAuthority(str));
}
}
return new UsernamePasswordAuthenticationToken(userId, null, authorities);
}
} catch (ExpiredJwtException e) {
// 异常捕获、发送到ExpiredJwtException
request.setAttribute("expiredJwtException", e);
// 将异常分发到ExpiredJwtException控制器
request.getRequestDispatcher("/expiredJwtException").forward(request, response);
} catch (UnsupportedJwtException e) {
// 异常捕获、发送到UnsupportedJwtException
request.setAttribute("unsupportedJwtException", e);
// 将异常分发到UnsupportedJwtException控制器
request.getRequestDispatcher("/unsupportedJwtException").forward(request, response);
} catch (MalformedJwtException e) {
// 异常捕获、发送到MalformedJwtException
request.setAttribute("malformedJwtException", e);
// 将异常分发到MalformedJwtException控制器
request.getRequestDispatcher("/malformedJwtException").forward(request, response);
} catch (SignatureException e) {
// 异常捕获、发送到SignatureException
request.setAttribute("signatureException", e);
// 将异常分发到SignatureException控制器
request.getRequestDispatcher("/signatureException").forward(request, response);
} catch (IllegalArgumentException e) {
// 异常捕获、发送到IllegalArgumentException
request.setAttribute("illegalArgumentException", e);
// 将异常分发到IllegalArgumentException控制器
request.getRequestDispatcher("/illegalArgumentException").forward(request, response);
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
}
自定义加密方式
@Component
@Slf4j
public class MySecurityPasswordEncoder implements PasswordEncoder {
@Autowired
private JwtConfig jwtConfig;
@Override
public String encode(CharSequence charSequence) {
String pwd = charSequence.toString();
String encode = AESUtil.encrypt(pwd, jwtConfig.getAesKey());
log.info("加密后:" + encode);
return encode;
}
@Override
public boolean matches(CharSequence charSequence, String s) {
String pwd = charSequence.toString();
// String md5Pwd = AESUtil.encrypt(pwd, jwtConfig.getAesKey());
if (pwd.equals(s)) {
return true;
}
log.info("--密码错误--");
return false;
}
}
登录后访问其他接口时的token校验
@Slf4j
public class JWTLoginFilter extends UsernamePasswordAuthenticationFilter {
private AuthenticationManager authenticationManager;
public JWTLoginFilter(AuthenticationManager authenticationManager) {
this.authenticationManager = authenticationManager;
}
/**
* 尝试身份认证(接收并解析用户凭证)
*
* @param req
* @param res
* @return
* @throws AuthenticationException
*/
@Override
public Authentication attemptAuthentication(HttpServletRequest req, HttpServletResponse res) throws AuthenticationException {
try {
UserEntity user = new ObjectMapper().readValue(req.getInputStream(), UserEntity.class);
return authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(
user.getUserName(),
user.getPassword(),
new ArrayList<>())
);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
/**
* 认证成功(用户成功登录后,这个方法会被调用,我们在这个方法里生成token)
*
* @param request
* @param response
* @param chain
* @param auth
* @throws IOException
* @throws ServletException
*/
@Override
protected void successfulAuthentication(HttpServletRequest request,
HttpServletResponse response,
FilterChain chain,
Authentication auth) throws IOException, ServletException {
// builder the token
String token = null;
try {
Collection<? extends GrantedAuthority> authorities = auth.getAuthorities();
// 定义存放角色集合的对象
List roleList = new ArrayList<>();
for (GrantedAuthority grantedAuthority : authorities) {
roleList.add(grantedAuthority.getAuthority());
}
token = JwtUtil.createJWT(auth.getName() + "-" + roleList);
// 生成token end
// 登录成功后,返回token到header里面
/*response.addHeader(ConstantKey.HEADER_KEY, ConstantKey.BEARER + token);*/
// 登录成功后,返回token到body里面
Map<String, Object> resultMap = new HashMap<>();
resultMap.put(ConstantKey.HEADER_KEY, ConstantKey.BEARER + token);
RestResponse result = RestResponse.success(resultMap);
response.getWriter().write(JSON.toJSONString(result));
} catch (Exception e) {
e.printStackTrace();
}
}
}
自定义用户登录时查询用户角色的方法
@Service
@Data
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
private UserMapper userMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
UserEntity user = userMapper.selectOne(new LambdaQueryWrapper<UserEntity>().eq(UserEntity::getUserName, username));
if (user == null) {
throw new UsernameNotFoundException("用户名未找到!");
}
List<GrantedAuthority> authorities = new ArrayList<>();
//查询用户角色
List<String> roleList = userMapper.selectRoleByUserId(user.getId());
if (!CollectionUtils.isEmpty(roleList)) {
for (String str : roleList) {
authorities.add(new SimpleGrantedAuthority(str));
}
}
return new LoginUser(user, authorities);
}
}
自定义身份验证,并生成令牌
@Component
public class CustomAuthenticationProvider implements AuthenticationProvider {
private UserDetailsService userDetailsService;
private PasswordEncoder bCryptPasswordEncoder;
public CustomAuthenticationProvider(UserDetailsService userDetailsService, PasswordEncoder passwordEncoder) {
this.userDetailsService = userDetailsService;
this.bCryptPasswordEncoder = passwordEncoder;
}
/**
* 执行与以下合同相同的身份验证
* {@link org.springframework.security.authentication.AuthenticationManager#authenticate(Authentication)}
* 。
*
* @param authentication 身份验证请求对象。
* @throws AuthenticationException 如果身份验证失败。
* @返回包含凭证的经过完全认证的对象。 可能会回来
* <code> null </ code>(如果<code> AuthenticationProvider </ code>无法支持)
* 对传递的<code> Authentication </ code>对象的身份验证。 在这种情况下,
* 支持所提供的下一个<code> AuthenticationProvider </ code>
* 将尝试<code> Authentication </ code>类。
*/
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
// 获取认证的用户名 & 密码
String name = authentication.getName();
String password = authentication.getCredentials().toString();
// 认证逻辑
LoginUser userDetails = (LoginUser) userDetailsService.loadUserByUsername(name);
if (null == userDetails) {
throw new UsernameNotFoundException("用户不存在!");
}
if (bCryptPasswordEncoder.matches(password, userDetails.getPassword())) {
// 这里设置权限和角色
List<GrantedAuthority> authorities = (List<GrantedAuthority>) userDetails.getAuthorities();
// 生成令牌 这里令牌里面存入了:name,password,authorities, 当然你也可以放其他内容
return new UsernamePasswordAuthenticationToken(userDetails.getUser().getId(), null, authorities);
} else {
throw new BadCredentialsException("密码错误!");
}
}
/**
* 是否可以提供输入类型的认证服务
*
* @param authentication
* @return
*/
@Override
public boolean supports(Class<?> authentication) {
return authentication.equals(UsernamePasswordAuthenticationToken.class);
}
}
最后就是在登录方法中通过令牌来生成JWT的token放回给前端
public RestResponse<UserTokenVo> login(Map<String, String> param) {
String userName = param.get("userName");
String password = param.get("password");
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(userName, AESUtil.encrypt(password, jwtConfig.getAesKey()));
Authentication authentication = authenticationManager.authenticate(authenticationToken);
if (Objects.isNull(authentication)) {
return new RestResponse<>(ResponseCodeEnum.UNAUTHORIZED);
}
SecurityContextHolder.setContext(new SecurityContextImpl(authentication));
//使用userid生成token
String userId = String.valueOf(authentication.getPrincipal());
//先在缓存中查找是否已登录,避免重复登录
String oldToken = redisTemplate.opsForValue().get(userId);
UserTokenVo token = new UserTokenVo();
if (StringUtils.isNotBlank(oldToken)) {
token = JSON.parseObject(oldToken, UserTokenVo.class);
} else {
String jwt = JwtUtil.createJWT(userId);
String refreshJwt = JwtUtil.createRefreshJWT(userId);
token.setRefreshToken(refreshJwt);
token.setAccessToken(jwt);
token.setUserName(userName);
token.setExpire(String.valueOf(JwtUtil.JWT_TTL));
redisTemplate.opsForValue().set("user_" + userId, JSON.toJSONString(token), JwtUtil.JWT_TTL, TimeUnit.MILLISECONDS);
}
//将用户的角色放入redis
redisTemplate.opsForValue().set("user_role_" + userId, JSON.toJSONString(authentication.getAuthorities().stream().map(r -> r.getAuthority()).collect(Collectors.toList())));
return RestResponse.success(token);
}
特别注意:角色名要ROLE_开头
后面就是将登录返回的token放入请求头信息中
Authorization:Bearer {token}
在controller中也可以加入注解
@PreAuthorize("hasRole('ADMIN')")
来表示只有角色是admin的才能访问该接口