spring boot +spring security 前后端分离项目
内容纲要
因为毕设设计项目需要,前后端分离和权限管理是必要的。
先占坑,功能已经实现,有时间再来填坑
更新,填坑,
第一部分,Configuration配置
SecurityConfig
类是继承了WebSecurityConfigurerAdapter
类,完成自定义登录验证方式。在这里我使用了sping session redis
进行对session的缓存,并实现单点登录.
@Configuration @EnableRedisHttpSession public class SecurityConfig extends WebSecurityConfigurerAdapter { //自定义提供用户和密码登录类 @Autowired private werls.scis.service.UserServiceImpl userService; //自定义密码验证类 @Autowired private PasswordEncoder passwordEncoder; @Autowired private AuthenticationProvider authenticationProvider; @Autowired AppAuthenticationSuccessHandler appAuthenticationSuccessHandler; @Autowired private AppAuthenticationFailureHandler appFailureHandler; @Autowired AppAccessDeniedHandler appAccessDeniedHandler; @Autowired private AppExpiredSessionStrategy appExpiredSessionStrategy; @Autowired MyLogoutSuccessHandler logoutSuccessHandler; @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } @Bean public AuthenticationProvider authenticationProvider() { DaoAuthenticationProvider provider = new DaoAuthenticationProvider(); provider.setHideUserNotFoundExceptions(false); provider.setUserDetailsService(userService); provider.setPasswordEncoder(passwordEncoder); return provider; } @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.authenticationProvider(authenticationProvider); } /** * 设置了登录页面,而且登录页面任何人都可以访问,然后设置了登录失败地址,也设置了注销请求,注销请求也是任何人都可以访问的。 * permitAll表示该请求任何人都可以访问,.anyRequest().authenticated(),表示其他的请求都必须要有权限认证。 * */ @Override protected void configure(HttpSecurity http) throws Exception { System.out.println("AppSecurityConfigurer configure http......"); System.out.println(new BCryptPasswordEncoder().encode("123456")); http.authorizeRequests() .antMatchers("/login","/register","/public/**","/home/**").permitAll() .antMatchers("/student/**","/i/**").hasAnyRole("STU","ADMIN", "TEA") .anyRequest().authenticated() .and() .formLogin() .loginPage("/login") .usernameParameter("username").passwordParameter("password") .successHandler(appAuthenticationSuccessHandler) .failureHandler(appFailureHandler) .permitAll() .and() .exceptionHandling() //没有权限,返回json .accessDeniedHandler(appAccessDeniedHandler) .and() .logout() .logoutSuccessHandler(logoutSuccessHandler) .permitAll() .and() .cors().disable() .csrf().disable()/*部署时需要关掉*/ .exceptionHandling() .and() .sessionManagement() .invalidSessionUrl("/login/invalid") .maximumSessions(1) .maxSessionsPreventsLogin(false) //当达到最大值时,旧用户被踢出后的操作 .expiredSessionStrategy(appExpiredSessionStrategy); } }
以上部分就是核心配置类,接下来是登录成功时返回的json类。在这里有能力的大佬可以使用token保持无状态连接,因为在spring security是默认使用session保持会话,想要实现的话需要自己重新验证过滤器。
AuthenticationSuccessHandler 实现类
@Component public class AppAuthenticationSuccessHandler implements AuthenticationSuccessHandler { private Logger logger = LoggerFactory.getLogger(getClass()); @Autowired private ObjectMapper objectMapper; /** * Called when a user has been successfully authenticated. * * @param request the request which caused the successful authentication * @param response the response * @param authentication the <tt>Authentication</tt> object which was created during */ @Override public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException { logger.info("登录成功"); Map<String, Object> map = new HashMap<String, Object>(); map.put("code", 200); map.put("message", "登录成功,正在转跳"); response.setContentType("application/json;charset=utf-8"); PrintWriter out = response.getWriter(); out.write(objectMapper.writeValueAsString(map)); out.flush(); out.close(); } }
AuthenticationFailureHandler 实现类
@Component public class AppAuthenticationFailureHandler implements AuthenticationFailureHandler { private Logger logger = LoggerFactory.getLogger(getClass()); @Autowired private ObjectMapper objectMapper; /** * Called when an authentication attempt fails. * * @param request the request during which the authentication attempt occurred. * @param response the response. * @param exception the exception which was thrown to reject the authentication */ @Override public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException { logger.info("登录失败"); response.setContentType("application/json;charset=utf-8"); PrintWriter out = response.getWriter(); Map<String,Object> map = new HashMap<String,Object>(); map.put("code",401); if (exception instanceof UsernameNotFoundException || exception instanceof BadCredentialsException) { map.put("message","用户名或密码错误"); } else if (exception instanceof DisabledException) { map.put("message","账户被禁用"); } else { map.put("message","登录失败!"); } out.write(objectMapper.writeValueAsString(map)); out.flush(); out.close(); } }
SessionInformationExpiredStrategy 实现类
@Component public class AppExpiredSessionStrategy implements SessionInformationExpiredStrategy { private ObjectMapper objectMapper = new ObjectMapper(); @Override public void onExpiredSessionDetected(SessionInformationExpiredEvent event) throws IOException, ServletException { Map<String,Object> map = new HashMap<>(); map.put("code",0); map.put("message","已经在另一台机器登录,您被迫下线。" + event.getSessionInformation().getLastRequest()); String s = objectMapper.writeValueAsString(map); event.getResponse().setContentType("application/json;charset=UTF-8"); event.getResponse().getWriter().write(s); } }
LogoutSuccessHandler 实现类
/** * @author : LiJiWei * @version V1.0 * @Project: scis * @Package werls.scis.security * @Description: TODO * @date Date : 2020年03月01日 22:25 */ @Component public class MyLogoutSuccessHandler implements LogoutSuccessHandler{ private Logger logger = LoggerFactory.getLogger(getClass()); @Autowired private ObjectMapper objectMapper; @Override public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException { Map<String,Object> map = new HashMap<String,Object>(); map.put("code",200); map.put("message","退出成功"); map.put("data",authentication); response.setContentType("application/json;charset=utf-8"); PrintWriter out = response.getWriter(); out.write(objectMapper.writeValueAsString(map)); out.flush(); out.close(); } }
AccessDeniedHandler 实现类
@Component public class AppAccessDeniedHandler implements AccessDeniedHandler { @Autowired private ObjectMapper objectMapper; /** * Handles an access denied failure. * * @param request that resulted in an <code>AccessDeniedException</code> * @param response so that the user agent can be advised of the failure * @param accessDeniedException that caused the invocation * @throws IOException in the event of an IOException * @throws ServletException in the event of a ServletException */ @Override public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException { response.setContentType("application/json;charset=utf-8"); response.setStatus(HttpServletResponse.SC_FORBIDDEN); PrintWriter out = response.getWriter(); Map<String,Object> map = new HashMap<String,Object>(); map.put("code",403); map.put("message", "权限不足"); out.write(objectMapper.writeValueAsString(map)); out.flush(); out.close(); } }
第二部分,前端登录
前端页面部分
我自己使用的vue.js作为前端,这个了给出的是vue下的。
- 这是HTML部分。
<div class="login-container" > <el-form ref="loginForm" :model="loginForm" :rules="loginRules" class="login-form" auto-complete="on" label-position="left"> <div class="title-container"> <h2 class="title">登 录</h2> </div> <el-form-item prop="username"> <span class="svg-container"> <svg-icon icon-class="user"/> </span> <el-input ref="username" v-model="loginForm.username" placeholder="学号/工号/身份号/手机/邮箱" name="username" type="text" tabindex="1" auto-complete="on" /> </el-form-item> <el-form-item prop="password3"> <span class="svg-container"> <svg-icon icon-class="password"/> </span> <el-input :key="passwordType" ref="password" v-model="loginForm.password" :type="passwordType" placeholder="密码" name="password" tabindex="2" auto-complete="on" @keyup.enter.native="handleLogin" /> <span class="show-pwd" @click="showPwd"> <svg-icon :icon-class="passwordType === 'password' ? 'eye' : 'eye-open'"/> </span> </el-form-item> <el-button :loading="loading" type="primary" style="width:100%;margin-bottom:30px;" @click.native.prevent="handleLogin">Login </el-button> <div style="position:relative"> <div class="tips"> <span> <el-link type="primary" @click="toHome">返回首页</el-link></span> </div> <div class="tips"> <span style="margin-right:18px;"> <el-link type="primary" >{{msg}}</el-link> </span> </div> <el-button class="thirdparty-button" type="primary" @click="passwordRecover"> 忘记密码 </el-button> </div> </el-form> </div>
- js部分
export default { name: "Login", data() { const validateUsername = (rule, value, callback) => { if (value.length < 1) { callback(new Error('请输入你的用户名')) } else { callback() } } const validatePassword = (rule, value, callback) => { if (value.length < 6) { callback(new Error('密码长度不能小于6位')) } else { callback() } } return { loginForm: { username: 'root', password: '123456'/*默认*/ }, loginRules: { username: [{required: true, trigger: 'blur', validator: validateUsername}], password: [{required: true, trigger: 'blur', validator: validatePassword}] }, passwordType: 'password', capsTooltip: false, loading: false, msg:'' } }, created() {//初始化后 }, mounted() {//创建vm.$el之后 /*判空*/ if (this.loginForm.username === '') { this.$refs.username.focus() } else if (this.loginForm.password === '') { this.$refs.password.focus() } }, destroyed() {//销毁之后 }, methods: { showPwd() { if (this.passwordType === 'password') { this.passwordType = '' } else { this.passwordType = 'password' } this.$nextTick(() => { this.$refs.password.focus() }) }, handleLogin() {/*登录前检验*/ this.$refs.loginForm.validate(valid => { if (valid) { this.loading = true this.$store.dispatch('user/login', this.loginForm) .then(response => { if (response.data.code === 401) { this.$notify.error({ title: '错误', message: response.data.message }); } else if (response.data.code === 200) { this.$notify.success({ title: '登录成功', message: response.data.message }); this.$router.push({path: '/home' || '/'}) } this.loading = false }) .catch(error => { this.$message.error("出现了一些问题" + error) this.loading = false }) } else { this.$notify.error({ title: '错误', message: "错误提交" }); return false } }) }, passwordRecover(){ this.$router.push({path:'/password/recover'}); console.log("忘记") }, toHome(){ console.log("home") this.$router.push({path:'/'}); } } }
- store部分
const actions = { // 用户登录 异步 login({commit}, userInfo) { return new Promise((resolve, reject) => { postFrom("/login", userInfo) .then(response => { const {data} = response commit('SET_TOKEN', data.token)//使用用户名 setToken(data.token) resolve(response) // this.getInfo(commit) }) .catch(errors => { reject(errors) }) }) }
- postFrom部分,这里很重要,因为没有对security的做修改,因此登录是无法解析json格式的,这里可以查看源码
export const postFrom =(url,params)=>{ return axios ({ method: 'post', url:'/api'+url, data:params, transformRequest:[function (data) { return qs.stringify(data) }], header: { 'Content-Type': 'application/x-www-form-urlencoded' } }) }
到此完成。