背景:
最近选型使用ruoyi的前后端分离框架进行研发,除了管理系统部分还有小程序部分功能,需要提供api给小程序部分,而小程序使用openid关联用户名,这样希望提供只根据用户名能够获取token进行后续后台服务接口的访问需求,具体改造如下:
增加5个关键类和一个测试类 修改一个config类,具体如下:
拦截特殊验证的拦截器类package com.ruoyi.framework.config.q; import java.io.IOException; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.springframework.security.authentication.AuthenticationServiceException; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter; import org.springframework.security.web.util.matcher.AntPathRequestMatcher; import org.springframework.util.StringUtils; public class QAuthenticationProcessingFilter extends AbstractAuthenticationProcessingFilter{ public static final String SPRING_SECURITY_FORM_USERNAME_KEY = "key"; private String keyParameter = SPRING_SECURITY_FORM_USERNAME_KEY; public QAuthenticationProcessingFilter(){ super(new AntPathRequestMatcher("/loginq", "GET")); } @Override public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException, ServletException{ String key = obtainKey(request); if (StringUtils.isEmpty(key)) { throw new AuthenticationServiceException("key不能为空"); } // TODO 根据key 查询username String username = key; return this.getAuthenticationManager().authenticate(new QAuthenticationToken(username)); } protected String obtainKey(HttpServletRequest request){ return request.getParameter(keyParameter); } }
验证实际处理类package com.ruoyi.framework.config.q; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.authentication.BadCredentialsException; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.authentication.dao.AbstractUserDetailsAuthenticationProvider; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; public class QAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider{ /** * 自定义用户认证逻辑 */ @Autowired private UserDetailsService userDetailsService; @Override protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException{ } @Override public Authentication authenticate(Authentication authentication) throws AuthenticationException{ QAuthenticationToken qToken = (QAuthenticationToken) authentication; try { UserDetails userDetails = userDetailsService.loadUserByUsername(qToken.getUserName()); return new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities()); } catch (Exception e) { logger.error(e); throw new BadCredentialsException("q登录异常:" + e.getMessage()); } } @Override protected UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException{ return null; } @Override public boolean supports(Class> authentication){ return (QAuthenticationToken.class.isAssignableFrom(authentication)); } }
验证通过后的处理类package com.ruoyi.framework.config.q; import java.io.IOException; import java.io.PrintWriter; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.security.core.Authentication; import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler; import com.alibaba.fastjson.JSONObject; import com.ruoyi.common.constant.Constants; import com.ruoyi.common.core.domain.AjaxResult; import com.ruoyi.common.utils.SecurityUtils; import com.ruoyi.framework.web.service.TokenService; public class QAuthenticationSuccessHandler extends SimpleUrlAuthenticationSuccessHandler{ @Autowired private TokenService tokenService; @Override public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException { //super.onAuthenticationSuccess(request, response, authentication); AjaxResult ajax = AjaxResult.success(); String token = tokenService.createToken(SecurityUtils.getLoginUser()); ajax.put(Constants.TOKEN, token); response.setContentType("application/json;charset=utf-8"); response.setStatus(HttpStatus.OK.value()); PrintWriter out = response.getWriter(); out.write(JSONObject.toJSONString(ajax)); out.flush(); out.close(); } }package com.ruoyi.framework.config.q; import java.io.IOException; import java.io.PrintWriter; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.springframework.http.HttpStatus; import org.springframework.security.core.AuthenticationException; import org.springframework.security.web.authentication.AuthenticationFailureHandler; import com.alibaba.fastjson.JSONObject; import com.ruoyi.common.core.domain.AjaxResult; public class QAuthenticationFailureHandler implements AuthenticationFailureHandler{ @Override public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException { AjaxResult ajax = AjaxResult.error(exception.getMessage()); response.setContentType("application/json;charset=utf-8"); response.setStatus(HttpStatus.OK.value()); PrintWriter out = response.getWriter(); out.write(JSONObject.toJSONString(ajax)); out.flush(); out.close(); } }
验证bean对象package com.ruoyi.framework.config.q; import org.springframework.security.authentication.AbstractAuthenticationToken; public class QAuthenticationToken extends AbstractAuthenticationToken{ private static final long serialVersionUID = 1L; private String userName; public QAuthenticationToken(String userName){ super(null); this.setUserName(userName); } @Override public Object getCredentials(){ return null; } @Override public Object getPrincipal(){ return null; } public String getUserName(){ return userName; } public void setUserName(String userName){ this.userName = userName; } }
测试类如下package com.ruoyi.framework.config.q; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; import com.ruoyi.common.core.domain.AjaxResult; @RestController public class QTestBizController{ /** * 登录方法 * * @param loginBody 登录信息 * @return 结果 */ @GetMapping("/loginq/test") public AjaxResult test() { AjaxResult ajax = AjaxResult.success(); ajax.put("test", "test123"); return ajax; } }
6.SecurityConfig配置中增加部分配置,详细差别请自行对比package com.ruoyi.framework.config; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.http.HttpMethod; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; 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.WebSecurityConfigurerAdapter; import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import org.springframework.security.web.authentication.logout.LogoutFilter; import org.springframework.web.filter.CorsFilter; import com.ruoyi.framework.config.q.QAuthenticationFailureHandler; import com.ruoyi.framework.config.q.QAuthenticationProcessingFilter; import com.ruoyi.framework.config.q.QAuthenticationProvider; import com.ruoyi.framework.config.q.QAuthenticationSuccessHandler; import com.ruoyi.framework.security.filter.JwtAuthenticationTokenFilter; import com.ruoyi.framework.security.handle.AuthenticationEntryPointImpl; import com.ruoyi.framework.security.handle.LogoutSuccessHandlerImpl; /** * spring security配置 * * @author ruoyi */ @EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true) public class SecurityConfig extends WebSecurityConfigurerAdapter{ /** * 自定义用户认证逻辑 */ @Autowired private UserDetailsService userDetailsService; /** * 认证失败处理类 */ @Autowired private AuthenticationEntryPointImpl unauthorizedHandler; /** * 退出处理类 */ @Autowired private LogoutSuccessHandlerImpl logoutSuccessHandler; /** * token认证过滤器 */ @Autowired private JwtAuthenticationTokenFilter authenticationTokenFilter; /** * 跨域过滤器 */ @Autowired private CorsFilter corsFilter; /** * 解决 无法直接注入 AuthenticationManager * * @return * @throws Exception */ @Bean @Override public AuthenticationManager authenticationManagerBean() throws Exception{ return super.authenticationManagerBean(); } /** * anyRequest | 匹配所有请求路径 * access | SpringEl表达式结果为true时可以访问 * anonymous | 匿名可以访问 * denyAll | 用户不能访问 * fullyAuthenticated | 用户完全认证可以访问(非remember-me下自动登录) * hasAnyAuthority | 如果有参数,参数表示权限,则其中任何一个权限可以访问 * hasAnyRole | 如果有参数,参数表示角色,则其中任何一个角色可以访问 * hasAuthority | 如果有参数,参数表示权限,则其权限可以访问 * hasIpAddress | 如果有参数,参数表示IP地址,如果用户IP和参数匹配,则可以访问 * hasRole | 如果有参数,参数表示角色,则其角色可以访问 * permitAll | 用户可以任意访问 * rememberMe | 允许通过remember-me登录的用户访问 * authenticated | 用户登录后可访问 */ @Override protected void configure(HttpSecurity httpSecurity) throws Exception{ httpSecurity // CSRF禁用,因为不使用session .csrf().disable() // 认证失败处理类 .exceptionHandling().authenticationEntryPoint(unauthorizedHandler).and() // 基于token,所以不需要session .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and() // 过滤请求 .authorizeRequests() // 对于登录login 验证码captchaImage 允许匿名访问 .antMatchers("/login", "/captchaImage").anonymous() .antMatchers( HttpMethod.GET, "/*.html", "/**/*.html", "/**/*.css", "/**/*.js" ).permitAll() .antMatchers("/profile/**").anonymous() .antMatchers("/common/download**").anonymous() .antMatchers("/common/download/resource**").anonymous() .antMatchers("/swagger-ui.html").anonymous() .antMatchers("/swagger-resources/**").anonymous() .antMatchers("/webjars/**").anonymous() .antMatchers("/*/api-docs").anonymous() .antMatchers("/druid/**").anonymous() // 除上面外的所有请求全部需要鉴权认证 .anyRequest().authenticated() .and() .headers().frameOptions().disable(); httpSecurity.logout().logoutUrl("/logout").logoutSuccessHandler(logoutSuccessHandler); // 添加JWT filter httpSecurity.addFilterBefore(qAuthenticationProcessingFilter(), UsernamePasswordAuthenticationFilter.class); httpSecurity.addFilterBefore(authenticationTokenFilter, UsernamePasswordAuthenticationFilter.class); // 添加CORS filter httpSecurity.addFilterBefore(corsFilter, JwtAuthenticationTokenFilter.class); httpSecurity.addFilterBefore(corsFilter, LogoutFilter.class); } @Bean public QAuthenticationProcessingFilter qAuthenticationProcessingFilter() throws Exception{ QAuthenticationProcessingFilter filter = new QAuthenticationProcessingFilter(); filter.setAuthenticationSuccessHandler(qAuthenticationSuccessHandler()); filter.setAuthenticationFailureHandler(qAuthenticationFailureHandler()); filter.setAuthenticationManager(super.authenticationManagerBean()); return filter; } @Bean public QAuthenticationSuccessHandler qAuthenticationSuccessHandler(){ // 不配置跳转地址,为了返回数据流 QAuthenticationSuccessHandler qAuthenticationSuccessHandler = new QAuthenticationSuccessHandler(); return qAuthenticationSuccessHandler; } @Bean public QAuthenticationFailureHandler qAuthenticationFailureHandler(){ // 不配置跳转地址,为了返回数据流 QAuthenticationFailureHandler qAuthenticationFailureHandler = new QAuthenticationFailureHandler(); return qAuthenticationFailureHandler; } /** * 强散列哈希加密实现 */ @Bean public BCryptPasswordEncoder bCryptPasswordEncoder(){ return new BCryptPasswordEncoder(); } @Bean public QAuthenticationProvider qAuthenticationProvider(){ return new QAuthenticationProvider(); } /** * 身份认证接口 */ @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception{ auth.authenticationProvider(qAuthenticationProvider()); auth.userDetailsService(userDetailsService).passwordEncoder(bCryptPasswordEncoder()); } }
测试效果方式如下:
通过访问
http://localhost:8088/loginq?key=admin获取token
正确结果是:
{"msg":"操作成功","code":200,"token":"
eyJhbGciOiJIUzUxMiJ9.eyJsb2dpbl91c2VyX2tleSI6ImFjNTcwMmJhLWQyYTItNDBlZS1hYjVkLTI2ZDExYjU4Yzk5OSJ9.R2ROekesiKL1fngPletnl6WfSqka-GGKq0zkBrHfnjIyAyjGtaENOM6L_AJKI5c4XXEBr0iOl44chY4RqJbwlw"}
如果未获取到结果如下:
{"msg":"key不能为空","code":500}
通过访问
http://localhost:8088/loginq/test并且在header头中增加Authorization为key,值为上一个返回的token执行测试