Spring Security

spring Security前后端分离返回token信息

2020-07-24  本文已影响0人  King斌

后端项目地址:https://gitee.com/kitter/vd-mall

前端项目地址:https://gitee.com/kitter/vd-mall-web

添加Security配置类

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
@PropertySource("classpath:security-config.properties")
@Slf4j
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    @Value("${security.ignore.resource}")
    private String[] securityIgnoreResource;
 
    @Value("${security.ignore.api}")
    private String[] securityIgnoreApi;
 
    @Value("${security.login.url}")
    private String loginApi;
 
    @Value("${security.logout.url}")
    private String logoutApi;
 
    @Value("${security.login.username.key:username}")
    private String usernameKey;
 
    @Value("${security.login.password.key:password}")
    private String passwordKey;
 
    @Autowired
    UserDetailService userDetailService;
 
    @Override
    protected void configure(HttpSecurity httpSecurity) throws Exception {
        httpSecurity.csrf().disable()
                .authorizeRequests()
                //对于静态资源的获取允许匿名访问
                .antMatchers(HttpMethod.GET, securityIgnoreResource).permitAll()
                // 对登录注册要允许匿名访问;
                .antMatchers(securityIgnoreApi).permitAll()
                //其余请求全部需要登录后访问
                .anyRequest().authenticated()
                //这里配置的loginProcessingUrl为页面中对应表单的 action ,该请求为 post,并设置可匿名访问
                .and().formLogin().loginProcessingUrl(loginApi).permitAll()
                //这里指定的是表单中name="username"的参数作为登录用户名,name="password"的参数作为登录密码
                .usernameParameter(usernameKey).passwordParameter(passwordKey)
                //登录成功后的返回结果
                .successHandler(new AuthenticationSuccessHandlerImpl())
                //登录失败后的返回结果
                .failureHandler(new AuthenticationFailureHandlerImpl(usernameKey))
                //这里配置的logoutUrl为登出接口,并设置可匿名访问
                .and().logout().logoutUrl(logoutApi).permitAll()
                //登出后的返回结果
                .logoutSuccessHandler(new LogoutSuccessHandlerImpl())
                //这里配置的为当未登录访问受保护资源时,返回json
                .and().exceptionHandling().authenticationEntryPoint(new AuthenticationEntryPointHandler());
    }
 
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailService).passwordEncoder(passwordEncoder());
    }
 
    @Bean
    public PasswordEncoder passwordEncoder() {
        //配置密码加密,这里声明成bean,方便注册用户时直接注入
        return new BCryptPasswordEncoder();
    }
}
 

@EnableWebSecurity:开启Security,该注解中包含@Import注解,使得WebSecurityConfiguration配置类生效
@EnableGlobalMethodSecurity(prePostEnabled = true):开启访问权限
@PropertySource("classpath:security-config.properties") 这里自定义的一个properties文件,通过@PropertySource注解导入后可以用@Value 读取里面的值,我这里配置的主要是SwaggerUI静态资源的忽略 以及登入登出的api地址,由于这类配置几乎不会被修改,因此这里直接独立出来一分配置文件

security.ignore.resource=/swagger-resources/**, /v2/api-docs/**, /webjars/springfox-swagger-ui/**, /swagger-ui.html,/**/*.js
security.ignore.api=/admin/api/v1/users/register
security.login.url=/admin/api/v1/users/login
security.logout.url=/admin/api/v1/users/logout

上面我们提到过前后端分离我们希望所有的返回结果都以json的方式返回给前台。但是Spring Security 默认返回页面,这里我们特殊处理下。我们可以看到在 Spring Security 配置类中 有几个Handler,这里就是我们需要自己实现的地方:

AuthenticationSuccessHandlerImpl.java 这个类定义登录成功的返回结果

@Slf4j
public class AuthenticationSuccessHandlerImpl implements AuthenticationSuccessHandler {
    @Override
    public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
        //登录成功后获取当前登录用户
        UserDetail userDetail = (UserDetail) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
        log.info("用户[{}]于[{}]登录成功!", userDetail.getUser().getUsername(), new Date());
        WriteResponse.write(httpServletResponse, new SuccessResponse());
    }
}

AuthenticationFailureHandlerImpl.java 这个类定义登录失败的返回结果,我们区分了登录失败的类型

@Slf4j
public class AuthenticationFailureHandlerImpl implements AuthenticationFailureHandler {
    private String usernameKey;
 
    public AuthenticationFailureHandlerImpl(String usernameKey) {
        this.usernameKey = usernameKey;
    }
 
    @Override
    public void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
        GlobalResponseCode code;
 
        if (e instanceof BadCredentialsException || e instanceof UsernameNotFoundException) {
            code = GlobalResponseCode.USERNAME_OR_PASSWORD_ERROR;
        } else if (e instanceof LockedException) {
            code = GlobalResponseCode.ACCOUNT_LOCKED_ERROR;
        } else if (e instanceof CredentialsExpiredException) {
            code = GlobalResponseCode.CREDENTIALS_EXPIRED_ERROR;
        } else if (e instanceof AccountExpiredException) {
            code = GlobalResponseCode.ACCOUNT_EXPIRED_ERROR;
        } else if (e instanceof DisabledException) {
            code = GlobalResponseCode.ACCOUNT_DISABLED_ERROR;
        } else {
            code = GlobalResponseCode.LOGIN_FAILED_ERROR;
        }
        RestResponse response = new ErrorResponse(code);
        String username = httpServletRequest.getParameter(usernameKey);
        log.info("用户[{}]于[{}]登录失败,失败原因:[{}]", username, new Date(), response.getMessage());
 
        WriteResponse.write(httpServletResponse, response);
    }
}
 

LogoutSuccessHandlerImpl.java 这个类定义登出返回结果

@Slf4j
public class LogoutSuccessHandlerImpl implements LogoutSuccessHandler {
    @Override
    public void onLogoutSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
        if (authentication != null) {
            log.info("用户[{}]于[{}]注销成功!", ((UserDetail) authentication.getPrincipal()).getUsername(), new Date());
        }
       
        WriteResponse.write(httpServletResponse, new SuccessResponse());
    }
}
 

AuthenticationEntryPointHandler.java 这里配置的为当未登录访问受保护资源时,返回json

public class AuthenticationEntryPointHandler implements AuthenticationEntryPoint {
   @Override
   public void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
       WriteResponse.write(httpServletResponse,  new ErrorResponse(GlobalResponseCode.ACCESS_FORBIDDEN_ERROR));
   }
}
WriteResponse.java 将返回内容写入HttpServletResponse 

class WriteResponse {
   private static final ObjectMapper mapper = new ObjectMapper();

   static void write(HttpServletResponse httpServletResponse, RestResponse restResponse) throws IOException {
       httpServletResponse.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);
       PrintWriter out = httpServletResponse.getWriter();
       out.write(mapper.writeValueAsString(restResponse));
       out.flush();
       out.close();
   }
}

到这里我们的配置已经基本完成,当然可能有人会问用户登录时是怎么连接数据库做查询的,这里就是我们接下来要说的。

首先创建我们的用户类 User.java

@Data
public class User {
    private int id;
    private String username;
    private String password;
    private String nickname;
    private String avatar;
    private int sex;
    private boolean accountNonExpired;
    private boolean accountNonLocked;
    private boolean credentialsNonExpired;
    private boolean enabled;
    private Date updateTime;
    private Date createTime;
}

创建 UserDetail.java 类 继承 UserDetails ,UserDtails 类是 Security 中对用户的抽象,包含用户的基本信息,以及角色信息

public class UserDetail implements UserDetails {
    private User user;
    private List<String> roles;
 
    public List<String> getRoles() {
        return roles;
    }
 
    public void setRoles(List<String> roles) {
        this.roles = roles;
    }
 
    public User getUser() {
        return user;
    }
 
    public void setUser(User user) {
        this.user = user;
    }
 
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        if (roles == null || roles.isEmpty()) {
            return new ArrayList<>();
        }
 
        List<GrantedAuthority> authorities = new ArrayList<>(roles.size());
        for (String role : roles) {
            authorities.add(new SimpleGrantedAuthority(role));
        }
 
        return authorities;
    }
 
    @Override
    public String getPassword() {
        return user.getPassword();
    }
 
    @Override
    public String getUsername() {
        return user.getUsername();
    }
 
    @Override
    public boolean isAccountNonExpired() {
        return user.isAccountNonExpired();
    }
 
    @Override
    public boolean isAccountNonLocked() {
        return user.isAccountNonLocked();
    }
 
    @Override
    public boolean isCredentialsNonExpired() {
        return user.isCredentialsNonExpired();
    }
 
    @Override
    public boolean isEnabled() {
        return user.isEnabled();
    }
}

创建 UserDetailService 继承 UserDetailsService 类,根据用户名查询用户信息

@Service
public class UserDetailService implements UserDetailsService {
    @Autowired
    UserDao userDao;
 
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        UserDetail userDetail = userDao.getUserDetailsByUserName(username);
        if (userDetail == null) {
            throw new UsernameNotFoundException("Not found username:" + username);
        }
        return userDetail;
    }
}

配置Security 的认证管理器,在我们前面的配置类中重写 WebSecurityConfigurerAdapter 的protected void configure(AuthenticationManagerBuilder auth) 方法即可

@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
   auth.userDetailsService(userDetailService).passwordEncoder(passwordEncoder());
}

vue 实现登录功能集成
这里就不具体讲述Vue的详细流程了后面开单章进行讲述,这里看下效果吧,可以看到登录成功之后sessionid已经写入到了Cookies中。

上一篇下一篇

猜你喜欢

热点阅读