所遇到的问题及解决方法
一、spring-boot集成spring-security
1.导入依赖
<!--spring-security权限认证框架-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
2.security的配置
其功能很强大,只需要配置少量信息就可以,配置类如下
配置类
package com.zy.collect.config;
import com.zy.collect.service.impl.CustomUserDetailsService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
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.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
/**
* 安全模块即登录验证板块配置类
*/
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private CustomUserDetailsService userDetailsService;
@Autowired
private BCryptPasswordEncoder bCryptPasswordEncoder;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService)
.passwordEncoder(bCryptPasswordEncoder);
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
// 如果有允许匿名的url,填在下面
// .antMatchers("/static/**").permitAll()
.antMatchers("/admin/**")//控制路径指定
.hasRole("ADMIN")//角色指定
.and()
// 设置登陆页
.formLogin().loginPage("/admin/login")
// 设置登陆成功页
.defaultSuccessUrl("/admin/index?pageNum=1&pageSize=5",true)
// 设置登录失败页面
.failureUrl("/admin/login?error=true")
.permitAll()
// 自定义登陆用户名和密码参数,默认为username和password
// .usernameParameter("username")
// .passwordParameter("password")
.and()
.logout().permitAll();
// 关闭CSRF跨域
http.csrf().disable();
}
@Override
public void configure(WebSecurity web) throws Exception {
// 设置拦截忽略文件夹,可以对静态资源放行
// web.ignoring().antMatchers("/css/**", "/js/**");
web.ignoring().antMatchers("/static/**");
}
}
注意:设置登录成功页面时切记带上后面的true参数,不然根据源码会将该参数定为false导致登录成功页面设置失败
虽然security模块有自身的登录页面但是样子不够特别,所有在配置类中可以自己定义登录页面,以及登录成功的页面还有登录失败的页面,一般情况下登录失败需要重新跳转到登录页面,故为了给提示,在配置失败页面url时加入error=true属性,方便控制器判断是否给出登录失败提示,登录页面 (主体) 与登录控制器如下:
页面与控制器
<form class="ui large form" method="post" action="#" th:action="@{/admin/login}">
<div class="ui segment">
<div class="field">
<div class="ui left icon input">
<i class="user icon"></i>
<input type="text" name="username" placeholder="用户名">
</div>
</div>
<div class="field">
<div class="ui left icon input">
<i class="lock icon"></i>
<input type="password" name="password" placeholder="密码">
</div>
</div>
<button class="ui fluid large teal submit button">登 录</button>
</div>
<div class="ui mini error message"></div>
<div class="ui mini negative message" th:unless="${#strings.isEmpty(message)}" th:text="${message}">用户名或密码错误</div>
</form>
@Controller
@RequestMapping("/admin")
public class AdminLoginController {
/**
* 配合security进行登录验证
* @param error
* @param model
* @return
*/
@GetMapping("/login")
public String loginPage(@RequestParam(name = "error",required = false) Boolean error, Model model){
if(error != null && error){
model.addAttribute("message","用户名或密码错误,请重新输入");
}
return "admin/login";
}
}
其中在登录失败跳转过来后就会进入if语句使用Model存储message信息并在登录页面展示
在配置类中需要注入的账户信息获取的业务层类
package com.zy.collect.service.impl;
import com.zy.collect.domain.Admin;
import com.zy.collect.service.IAdminService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.ArrayList;
import java.util.Collection;
/**
* 登录验证业务层实现类
*/
@Transactional
@Service
@Slf4j
public class CustomUserDetailsService implements UserDetailsService {
@Autowired
IAdminService adminService;
@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
Collection<GrantedAuthority> authorities = new ArrayList<>();
Admin admin = adminService.findByName(s);
log.info("admin:{}",admin);
if(admin == null){
throw new UsernameNotFoundException("用户不存在");
}
authorities.add(new SimpleGrantedAuthority(admin.getAdmin()));
return new User(admin.getUsername(),admin.getPassword(),authorities);
}
}
mapper接口
/**
* 根据账号获取管理员信息
* @param username
* @return
*/
@Select("select * from admin where username = #{username}")
Admin findByUsernameAndPassword(String username);
信息封装的类
package com.zy.collect.domain;
//省略get与set方法
public class Admin {
private Long id;
private String username; //账号
private String password; //密码
private String admin; //角色
}
可以看到我吧所需要的账号、密码、角色信息都放在了一个类,方便进行查找与修改。
在配置类中注入账户信息的时候采用了
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService)
.passwordEncoder(bCryptPasswordEncoder);
}
这代表密码采用加密的形式进行存储的。
因为管理员少,故我直接在test下,使用bCryptPasswordEncoder类直接生成加密后的密码然后存入数据库,如下:
生成密文代码
package com.zy.collect;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
@SpringBootTest
class CollectApplicationTests {
@Autowired
private BCryptPasswordEncoder pwdEncoder;
@Test
void contextLoads() {
String password = "**********";//此处为需要设置的密码
//生成密文
String encode = pwdEncoder.encode(password);//此处生成密文
System.out.println(encode);
String encode1 = pwdEncoder.encode(password);//此处生成密文
System.out.println(encode1);
//验证密文
boolean b = pwdEncoder.matches(password, encode);
boolean b1 = pwdEncoder.matches(password, encode1);
System.out.println(b);
System.out.println(b1);
}
}
如上spring-security登录验证功能集成完毕
二、spring-boot集成pagehelper分页
1.导入依赖
<!-- pagehelper分页 -->
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper-spring-boot-starter</artifactId>
<version>1.2.5</version>
</dependency>
2.配置
为了不耦合,采用自己写的类对分页请求与分页结果进行包装:
分页请求包装类
package com.zy.collect.util;
/**
* 分页请求包装类
*/
//省略get与set方法
public class PageRequest {
private Integer pageNum; //当前页码
private Integer pageSize; //每页数量
}
分页结果包装类
package com.zy.collect.util;
import java.util.List;
/**
* 分页返回结果包装类
*/
//省略get与set方法
public class PageResult {
private Integer pageNum; //当前页码
private Integer pageSize; //每页数量
private Long totalSize; //记录总数
private Integer totalPages; //页码总数
private List<?> content; //数据模型
}
分页工具类
package com.zy.collect.util;
import com.github.pagehelper.PageInfo;
/**
* 分页查询相关工具类。
*/
public class PageUtils {
public static PageResult getPageResult(PageRequest pageRequest, PageInfo<?> pageInfo){
PageResult pageResult = new PageResult();
pageResult.setPageNum(pageInfo.getPageNum());
pageResult.setPageSize(pageInfo.getPageSize());
pageResult.setTotalSize(pageInfo.getTotal());
pageResult.setTotalPages(pageInfo.getPages());
pageResult.setContent(pageInfo.getList());
return pageResult;
}
}
3.分页插件使用
配置文件内容
pagehelper:
helperDialect: mysql
reasonable: true
supportMethodsArguments: true
params: count=countSql
mapper层
和普通的查询所有语句一样
/**
* 查询所有用户信息并分页
* @return
*/
@Select("select * from users")
List<User> selectPage();
service层
/**
* 分页查询用户信息
* @param pageRequest 自定义,统一分页查询请求
* @return
*/
@Override
public PageResult findPage(PageRequest pageRequest) {
return PageUtils.getPageResult(pageRequest,getPageInfo(pageRequest));
}
/**
* 分页插件载入
* @param pageRequest
* @return
*/
private PageInfo<User> getPageInfo(PageRequest pageRequest){
Integer pageNum = pageRequest.getPageNum();
Integer pageSize = pageRequest.getPageSize();
PageHelper.startPage(pageNum,pageSize);//必须在调用查询方法的紧上方
List<User> users = userMapper.selectPage();
return new PageInfo<User>(users);
}
web层
/**
* 首页
* 所有用户信息分页展示
* 每页5条用户信息
* @param pageRequest
* @param model
* @return
*/
@GetMapping("/index")
public String index(PageRequest pageRequest,
Model model){
PageResult page = adminService.findPage(pageRequest);
model.addAttribute("page",page);
return "admin/adminPage";
}
调用该请求时,必须带上pageNum和pageSize属性,会自动封装到pageRequest参数里
例如:/admin/index?pageNum=1&pageSize=5
使用thymeleaf则为@{/admin/index(pageNum=1,pageSize=5)}
页面
正常接收就行,可以知道数据都在PageResult的content属性中,并且PageResult提供了许多分页页面所需的信息
三、spring-boot集成thymeleaf模板
1.导入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
2.使用
配置使用默认的即可,
然后再页面导入
xmlns:th="http://www.thymeleaf.org"
即
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
......
......
就可以使用其功能,非常方便
四、spring-boot跨域问题
当协议、域名、端口号有一样不同时就算是跨域,在springboot中有专门解决跨域访问的注解@CrossOrigin,只要在需要跨域的controller方法上加上此注解就可以实现跨域访问,也可以加在类上,代表该controller的所有方法均支持跨域访问
五、spring-boot接收参数与返回数据问题
1.接收参数
在web层controller方法接收的参数一般都来源自页面的form表单获取访问url后面的参数。问题如下:
form表单与url参数同时存在需要注意的问题
1.form表单与url参数的名称不能相同,如果相同,即使form表单没有给此参数赋值,在获取参数值时也会从url参数中获取该值,从而造成参数混乱,即使使用封装好的对象获取form数据依旧会存在该问题
2.不过或参数名一致,且二者都有值,会优先从form表单取出数据,既然如此,是不是可以使用url参数给定form表单的数据默认值?仅猜想!
form表单数据相对于接收的实体类不全时
1.实例类属性在form表单中,只是没有给定数据时,会返回一个空字符串,而不是null。
2.实体类属性不在form表单中,若属性类型为字符串则要额外注意,因为此时给属性会被赋值成‘null’,注意不是空,而只是一个普通字符串,但在后续使用时会报空指针异常。
针对以上两点,都需要进行字符串是否相等的判断,而不能只判空就使用
2.返回数据
方法或类上使用@RestController注解
使用该注解表明返回的是数据,而不需要页面渲染,所以一般使用Map集合储存数据即
Map<String, Object> map = new HashMap<>();
采用键值对的方式储存数据,这种数据对应着返回去的response.data.*数据,使用起来十分方便
使用注意数据类型与前端使用方式进行匹配
方法或类上使用@Controller注解
此方法的返回值就是一个页面获取一个控制器的处理地址,先会去匹配是否有对应页面,没有的话就会去匹配是否有对应控制器,都没有就会抛出异常。
1.在做页面跳转时使用转发的话可以使用Model进行携带数据的存储。使用重定向的话需要使用RedirectAttributes进行携带的数据的存储。
2.重定向使用attributes.addAttribute()时相当于拼接在重定向的url上,可以通过获取url参数的方式获取到该数据,使用attributes.addFlashAttribute()则只能通过Map获取
六、spring-boot监听器载入
此次使用了用于监听session生命周期的监听类,用来侦查session的创建与销毁
session监听器类
package com.zy.collect.servlet;
import com.zy.collect.context.MySessionContext;
import javax.servlet.http.HttpSession;
import javax.servlet.http.HttpSessionEvent;
import javax.servlet.http.HttpSessionListener;
/**
* session监听器类
*/
public class MySessionListener implements HttpSessionListener {
public void sessionCreated(HttpSessionEvent httpSessionEvent) {
MySessionContext.AddSession(httpSessionEvent.getSession());
}
public void sessionDestroyed(HttpSessionEvent httpSessionEvent) {
HttpSession session = httpSessionEvent.getSession();
MySessionContext.DelSession(session);
}
}
该类中使用了自定义工具类,实现session的储存、销毁与获取
session工具类
package com.zy.collect.context;
import javax.servlet.http.HttpSession;
import java.util.HashMap;
/**
* session的Map集合类
*/
public class MySessionContext {
private static HashMap<String,Object> map = new HashMap();
/**
* 保存session
* @param session
*/
public static synchronized void AddSession(HttpSession session) {
if (session != null) {
map.put(session.getId(), session);
}
}
/**
* 删除过期session
* @param session
*/
public static synchronized void DelSession(HttpSession session) {
if (session != null) {
map.remove(session.getId());
}
}
/**
* 根据id取出session
* @param session_id
* @return
*/
public static synchronized HttpSession getSession(String session_id) {
if (session_id == null)
return null;
return (HttpSession) map.get(session_id);
}
}
虽然已经有了监听器,但监听器需要交给spring管理才能生效,采用通过创建RegistrationBean子类方式实现
配置监听器
package com.zy.collect.config;
import com.zy.collect.servlet.MySessionListener;
import org.springframework.boot.web.servlet.ServletListenerRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* 监听器载入
*/
@Configuration
public class MyRegistConfig {
@Bean
public ServletListenerRegistrationBean myListener(){
MySessionListener listener = new MySessionListener();
return new ServletListenerRegistrationBean(listener);
}
}
springboot注册servlet、filter、listener、intercepter的方法总结
对于servlet、filter、listener有目前我所知道的方法有两种通过注解或者通过注入RegistrationBean子类的方法实现。
对于intercepterspringMVC可以通过集成其配置类直接加进去,具体如下
intercepter
创建一个配置类实现WebMvcConfigurer 接口实现addInterceptors方法将拦截器配置进去,其中LoginInterceptor为自定义的拦截器
@Configuration
public class AdminWebConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LoginInterceptor())
.addPathPatterns("/**") //拦截所有请求,包括静态资源
.excludePathPatterns("/","/login","/css/**","/fonts/**","/images/**","/js/**","/sql","/findAll"); //不拦截路径
}
}
filter
注解实现
想要通过注解进行配置,首先注解也需要装配,不然注解起不到效果,装配需要在启动类上加上@ServletComponentScan注解,servlet、listener注解配置同理!
package com.zy.admin;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.web.servlet.ServletComponentScan;
@ServletComponentScan
@SpringBootApplication
public class BootWebAdminApplication {
public static void main(String[] args) {
SpringApplication.run(BootWebAdminApplication.class, args);
}
}
filter类
package com.zy.admin.servlet;
import lombok.extern.slf4j.Slf4j;
import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import java.io.IOException;
/**
* 注入过滤器
*/
@Slf4j
@WebFilter(urlPatterns = {"/css/*","/images/*"})//需要该过滤器的路径
public class MyFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
log.info("MyFilter初始化完成");
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
log.info("MyFilter工作");
filterChain.doFilter(servletRequest,servletResponse);
}
@Override
public void destroy() {
log.info("MyFilter销毁");
}
}
注入RegistrationBean子类实现
不使用注解
@Bean
public FilterRegistrationBean myFilter(){
MyFilter myFilter = new MyFilter();
FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean(myFilter);
filterRegistrationBean.setUrlPatterns(Arrays.asList("/my","/css/*"));//需要该过滤器的路径
return filterRegistrationBean;
}
servlet
注解实现
package com.zy.admin.servlet;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* 注入servlet
*/
@WebServlet(urlPatterns = "/my")//访问路径
public class MyServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.getWriter().write("66666");
}
}
注入RegistrationBean子类实现
@Bean
public ServletRegistrationBean myServlet(){
MyServlet myServlet = new MyServlet();
return new ServletRegistrationBean(myServlet,"/my");//访问路径
}
listener
注解实现
package com.zy.admin.servlet;
import lombok.extern.slf4j.Slf4j;
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
import javax.servlet.annotation.WebListener;
/**
* 注入监听器
*/
@Slf4j
@WebListener
public class MyServletContextListener implements ServletContextListener {
@Override
public void contextInitialized(ServletContextEvent sce) {
log.info("MyServletContextListener监听到项目初始化完成");
}
@Override
public void contextDestroyed(ServletContextEvent sce) {
log.info("MyServletContextListener监听到项目销毁");
}
}
注入RegistrationBean子类实现
@Bean
public ServletListenerRegistrationBean myListener(){
MyServletContextListener listener = new MyServletContextListener();
return new ServletListenerRegistrationBean(listener);
}
七、其他小问题
spring-boot的favicon.ico显示问题
favicon.ico文件存放位置必须在配置的静态文件夹的根目录下。如果确认自己配置好着,可以再去target下对应目录看看有没有,没有的话说明配置还有问题,若此处还没有问题,那么可以就是 浏览器没有重新启动的问题了!!! 切记重新启动浏览器再测试。
@RequestBody注解使用注意事项
1.@RequestBody注解用来获取请求体中的数据,直接使用得到的是key=value&key=value…结构的数据,因此get方式不适用(get方式下@RequestBody获取不到任何数据)。
2.使用@RequestBody注解后,可以在方法中创建一个集合对象,前端提交的集合数据可以直接被注入到方法的集合对象中,而不需要创建一个pojo对象进行集合的封装。
3.如果想要将前端提交的json字符串自动封装到一个对象中,需要导入jackson的相关jar包,并使用@RequestBody注解。如果不导入该模块jar包则springboot使MappingJacksonHttpMessageConverter对json数据进行转换导致不能封装到对象中而且会报错!!!
4.使用@RequestBody 前后端参数要匹配个数不能少。字段名字要一样。
综上,@RequestBody注解的使用十分严格,对应那些form表单可以有参数不传值的就不需要用@RequestBody注解,而且多导入一个依赖也不是好选择,可以在不使用@RequestBody注解的时候直接用对象接收form的参数,只是不能保证接收对象的属性一定有值,从而要自己做许多判断。
可以理解为@RequestBody就是一种提交表单的规范,可以节省之后的数据处理的时间,不过使用时一定要注意使用条件。