前言:SpringSecurity自带了自己的session验证、登录页面、登录方式等等功能,要想用token实现前后分离也可以用Security-JWT来实现。但是官方自带的这些功能我个人很不习惯,我还是习惯用redis和自定义token去实现权限判断。所以这次学习Security并集成到我的项目时进行了魔改(其实是官方给的接口重新实现而已)
在SecurityConfiguration配置bean继承WebSecurityConfigurerAdapter的类中实现父类configure方法,我这里这样配置的:
protected void configure(HttpSecurity http) throws Exception {
log.info("初始化安全框架配置...");
//启用自定义登录页
var httpc = http
.formLogin()
.loginPage("/admin/login");
//路径白名单
Set<String> userTokenInterceptors = applicationAuthorityBeen.whitePaths();
userTokenInterceptors.addAll(Arrays.asList(whitePaths));
httpc.and()
.authorizeRequests()
.antMatchers(userTokenInterceptors.toArray(String[]::new))
.permitAll();
//路径权限注册
var httpd = httpc.and()
.authorizeRequests();
Map<String, Set<String>> stringSetMap = applicationAuthorityBeen.authConfig();
for (String url : stringSetMap.keySet()) {
httpd.antMatchers(url).hasAnyAuthority(stringSetMap.get(url).toArray(String[]::new));
}
// 其他的需要登录就能访问
httpd.anyRequest().authenticated();
// 取消跨站请求伪造防护
http.csrf().disable();
// 防止iframe 造成跨域
http.headers().frameOptions().disable();
//重写登录接口验证
http.addFilterAt(loginAuthGetRequestFilter(), UsernamePasswordAuthenticationFilter.class);
http.addFilterBefore(authenticationTokenFilter, LoginAuthGetRequestFilter.class);
http.httpBasic().authenticationEntryPoint(securityAuthenticationEntryPoint);
//禁用session
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
//无权限处理类
http.exceptionHandling().accessDeniedHandler(securityAccessDeniedHander);
//注销登录处理类
http.logout().logoutUrl("/auth/logout").logoutSuccessHandler(securityAccessLogoutHander);
}
这里面很多处理类接下来会一一说明
首先是applicationAuthorityBeen这个类主要通过自定义注解来获取路径白名单(whitePaths())和权限注册(authConfig())的集合,更方便对配置初始化操作,下面是相关代码
/**
* 检查所有been获取不需要拦截的ControllerPath
*
* @return 所有白名单路径
* @throws Exception
*/
public Set<String> whitePaths() throws Exception {
Map<String, Object> Beans = applicationContext.getBeansWithAnnotation(RestController.class);
log.info("获取ControllerPath白名单...");
Set<String> whitePaths = new HashSet<>();
for (String key : Beans.keySet()) {
//接口类名
Class clz = AopTargetUtils.getTarget(Beans.get(key)).getClass();
//获取其方法
for (Method method : clz.getMethods()) {
//权限注解对应
PathMatcher pathMatcher = method.getAnnotation(PathMatcher.class);
if (pathMatcher != null) {
//该接口是否是白名单
if (pathMatcher.white()) {
//获取访问地址
var ApiPath = getControllerPath(clz, method);
if (StrUtil.isNotBlank(ApiPath))
whitePaths.add(ApiPath);
}
}
}
}
return whitePaths;
}
它的注解是PathMatcher,可以在controller的方法上添加实现该方法的白名单过滤
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface PathMatcher {
//true为不过滤路径
boolean white() default true;
}
在Controller使用,比如登录页面需要加入白名单:
@GetMapping("/login")
@PathMatcher
public ModelAndView login() {
return JumpPage("login");
}
就OK了
authConfig()方法是获取controller的权限,就像shiro那样在注解直接写上权限来控制
public Map<String, Set<String>> authConfig() throws Exception {
log.info("获取Controller权限配置...");
Map<String, Object> Beans = applicationContext.getBeansWithAnnotation(RestController.class);
Map<String, Set<String>> authConfig = new HashMap<>();
for (String key : Beans.keySet()) {
//接口类名
Class clz = AopTargetUtils.getTarget(Beans.get(key)).getClass();
//获取其方法
for (Method method : clz.getMethods()) {
//权限注解对应
AccessAuth accessRole = method.getAnnotation(AccessAuth.class);
if (accessRole != null) {
//获取访问地址
var ApiPath = getControllerPath(clz, method);
if (StrUtil.isNotBlank(ApiPath)) {
Set<String> auths = new HashSet<>();
//接口的权限
String auth = accessRole.auth();
if (StrUtil.isNotBlank(auth))
auths.add(auth);
//该接口是否是ADMIN平台
if (accessRole.admin()) {
if (StrUtil.isBlank(auth))
auths.add("admin");
auths.add("superAdmin");
} else
auths.add("app");
authConfig.put(ApiPath, auths);
}
}
}
}
return authConfig;
}
在这里我通过判断是否是admin平台的controller来进行给予基本权限,它的注解是AccessAuth,下面是它的代码
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface AccessAuth {
//权限说明
String comment() default "";
//权限值
String auth() default "";
//是否是admin权限
boolean admin() default true;
}
在Controller使用:
1、只要是admin账号都能访问
@GetMapping("/index")
@AccessAuth(comment = "后台首页")
public ModelAndView index() {
return JumpPage("sys/console");
}
2、细分admin权限
@GetMapping("/index")
@AccessAuth(auth = "admin:index",comment = "后台首页")
public ModelAndView index() {
return JumpPage("sys/console");
}
3、非admin权限
@GetMapping("/index")
@AccessAuth(auth = "app:index",comment = "首页",admin = false)
public ModelAndView index() {
return JumpPage("index");
}
等等排列组合
以上通过自定义注解初始化权限就完事了,接下来是重写登录接口,原来官方的接口是formdata提交方式就很不'现代'hh 必须要改!LoginAuthGetRequestFilter这个类就是重写登录接口的类
1、在配置类中添加Bean:
@Bean
LoginAuthGetRequestFilter loginAuthGetRequestFilter() throws Exception {
LoginAuthGetRequestFilter filter = new LoginAuthGetRequestFilter();
filter.setAuthenticationSuccessHandler(securityAccessSuccessHander);
filter.setAuthenticationFailureHandler(securityAccessFailureHander);
filter.setFilterProcessesUrl("/auth/login");
filter.setAuthenticationManager(authenticationManagerBean());
return filter;
}
2、实现LoginAuthGetRequestFilter类
public class LoginAuthGetRequestFilter extends UsernamePasswordAuthenticationFilter {
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
if (!request.getMethod().equals("POST")) {
throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
}
String username = null;
String password = null;
try {
Map<String, String> requestValue = new ObjectMapper().readValue(request.getInputStream(), Map.class);
username = requestValue.get("username");
password = requestValue.get("password");
var remember = requestValue.get("remember");
request.setAttribute("username", username);
request.setAttribute("remember", remember);
} catch (Exception e) {
e.printStackTrace();
}
if (StrUtil.isBlank(username)) {
throw new AuthenticationServiceException("账号不能为空");
} else if (username.length() > 30) {
throw new AuthenticationServiceException("账号不能超过30字符");
}
if (StrUtil.isBlank(password)) {
throw new AuthenticationServiceException("密码不能为空");
} else if (password.length() > 20) {
throw new AuthenticationServiceException("账号不能超过20字符");
}
username = username.trim();
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
this.setDetails(request, authRequest);
return this.getAuthenticationManager().authenticate(authRequest);
}
}
这样就可以通过json把用户名密码传进来了,然后就是登录成功生成token了,其实这个没啥好说的根据自己的习惯去生成就行了,就在AuthenticationSuccessHandler这个接口实现基本就行了,都是基本操作就不费口舌了。接下来就是每次请求要验证的token操作了,authenticationTokenFilter这个类
public class AuthenticationTokenFilter extends OncePerRequestFilter {
private final Redis redis;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
var token = AccessTokenUtil.getTokenKey(request);
//获取用户信息
if (StrUtil.isNotBlank(token)) {
//获取到token了,就把这个用户信息解析出来吧!
SysUser userDetails =?;
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
//设置上下文为已登录
SecurityContextHolder.getContext().setAuthentication(authentication);
}
}
}
filterChain.doFilter(request, response);
}
}
这里我把token获取的方式封装了(AccessTokenUtil.getTokenKey),其实就是按照你自己的token传递方式来获取就行,header、cookie或直接传值都行,只要能取到就看你自己了
剩下的配置就是security基本操作了,总结一下,这通操作完实现了以下几个功能:
1、注解白名单自动注册到配置
2、根据我的需要实现注解权限控制
3、可以通过application/json传参来登录
4、实现定制化token生成和验证
完美!不仅是web模板开发还是分离开发都没问题