本文为原创文章,风格与你们见的不一样,那是因为,我喜欢将原理和注意事项写到代码的旁边,而不是你们所见的在引用中稍微讲解下就开始看代码,这样很容易迷糊和犯困,而且这样你们才能将代码和原理一一对应起来。此外创作不易,请多多支持和关注。
<!--pom.xml所需要的jar包-->
<!--security安全框架-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
首先需要继承安全适配器即WebSecurityConfigurerAdapter的类,并且将实现的各种类统一注入到该类中**(实现的功能注意注释)**
@Slf4j
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(
jsr250Enabled = true, //JSR-250注解
prePostEnabled = true, //spring表达式注解
securedEnabled = true //SpringSecurity注解,推荐使用
)//开启注解控制权限
public class AdminWebConfig extends WebSecurityConfigurerAdapter {
private ObjectMapper objectMapper = new ObjectMapper();
@Resource //(该类的作用下面会讲)
UserDetailsService service;
@Resource
private PasswordEncoder passwordEncoder;
@Bean
public PasswordEncoder passwordEncoder(){
// 使用BCrypt加密密码(根据业务要求,使用该方法)
return new BCryptPasswordEncoder();
}
//防止无法被 Spring 容器感知到,进而导致当用户注销登录之后导致用户无法重新登录进来
@Bean
HttpSessionEventPublisher httpSessionEventPublisher() {
return new HttpSessionEventPublisher();
}
//静态资源设置
@Override
public void configure(WebSecurity webSecurity) {
//不拦截静态资源,所有用户均可访问的资源
webSecurity.ignoring().antMatchers("/css/**","/img/**","/js/**","/expression/**,/face/**");
}
//HTTP请求安全处理(拦截请求)
@Override
protected void configure(HttpSecurity http) throws Exception {
//链式编程
http.authorizeRequests()
//为了防止你们弄混登录页面的url和执行登录过程的url,我特意区分开来的,看下面的代码 .antMatchers("/","/toLogin","/login").permitAll()//允许通过的url
.antMatchers("/vip/**").hasAuthority("vip")//url需要相关的权限才能通过
.antMatchers("/admin/**").hasAuthority("admin")
.anyRequest().authenticated()//对于任意的请求会拦截,除了开放的以外
.and().exceptionHandling().accessDeniedPage("/forbidden");//没有权限执行的url,将跳转到该页面去
//这里我需要声明的是,对于登录过程它会去执行哪个方法,就是需要实现AuthenticationProvider类,在下面会讲,请耐心看注释。
http.formLogin()
.loginProcessingUrl("/toLogin") //指定自定义form表单请求的路径(这里有个大坑,form表单中不能写action="***",只能写th:action="@{/toLogin}",th就是thymeleaf模板引擎的语法格式;不然你永远都不会登录成功)
.loginPage("/login") //指定登录页面的路径
.successHandler(new AuthenticationSuccessHandler() {
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
response.sendRedirect(request.getContextPath()+"/index");
}
})
.failureHandler(new AuthenticationFailureHandler() {
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException {
if (e instanceof SessionAuthenticationException){
request.setAttribute("error","已经登录过了,请不要重新登录");
request.getRequestDispatcher("/login").forward(request,response);
return;
}
request.setAttribute("error",e.getMessage());
request.getRequestDispatcher("/login").forward(request,response);
}
});
//.failureUrl("/undefinedError") //失败返回的url
http.csrf().disable(); //关闭csrf防护
// 禁用缓存
http.headers().cacheControl();
http.logout()
.logoutUrl("/logout")
.logoutSuccessHandler(new LogoutSuccessHandler(){
@Override
public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
response.sendRedirect("/toLogin");
}
});
//单用户登录,如果有一个登录了,同一个用户在其他地方登录将前一个剔除下线
http.sessionManagement().maximumSessions(1)
.maxSessionsPreventsLogin(true)
.expiredSessionStrategy(new SessionInformationExpiredStrategy() {
@Override
public void onExpiredSessionDetected(SessionInformationExpiredEvent event) throws IOException, ServletException {
Map<String, Object> map = new HashMap<>(16);
map.put("code", 0);
map.put("msg", "已经另一台机器登录,您被迫下线。" + event.getSessionInformation().getLastRequest());
// Map -> Json
String json = objectMapper.writeValueAsString(map);
event.getResponse().setContentType("application/json;charset=UTF-8");
event.getResponse().getWriter().write(json);
}
}).expiredUrl("/toLogin");
//单用户登录,如果有一个登录了,同一个用户在其他地方不能登录(自定义业务就不在这里写了,同上)
http.sessionManagement().maximumSessions(1).maxSessionsPreventsLogin(true);
}
//记住功能
http.rememberMe()
.rememberMeParameter("remeberme")//默认为remember-me,但我改成了remeberme,前端可以通过input的name="remeberme"拿到
.key("123")//指定Token识别字段
.tokenValiditySeconds(24*60*60)//remeber-me的cookie默认2周(14天)
.userDetailsService(service) //指定remember-me 功能自动登录过程使用的 UserDetailsService 对象(即下面即将讲的CustomUserDetailsService对象注入进来即可)
.authenticationSuccessHandler(new AuthenticationSuccessHandler(){
@Override
public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
//这里面可以写自定义的业务,然后返回给前端
}
});
}
继承安全适配器类过后,所以实现AuthenticationProvider 类才会起作用,而实现AuthenticationProvider 类的作用就是后端会将前端的信息拿到这里来判断。(判断过程注意注释)
//自定义认证规则
@Slf4j
@Component
public class VAuthenticationProvider implements AuthenticationProvider {
@Resource
CustomUserDetailsService service;
@Resource
HttpSession session;
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
//是否认证过
if (!authentication.isAuthenticated()){
String username=authentication.getName();
String password=authentication.getCredentials().toString();
UserDetails
//这里需要实现UserDetailsService类,该类的作用是将前端的信息拿到数据库中去判断userDetails=service.loadUserByUsername(username);
if (userDetails==null){
log.info("not find user");
throw new BadCredentialsException("用户没有找到");
}
if (!password.equals(userDetails.getPassword())) {
log.info("pwd is error");
throw new BadCredentialsException("密码错误");
}
session.setAttribute("loginUser",authentication.getPrincipal());
//认证校验通过后,封装UsernamePasswordAuthenticationToken返回
return new UsernamePasswordAuthenticationToken(userDetails, password, userDetails.getAuthorities());
}
return null;
}
//当前的AuthenticationProvider支持哪种类型认证
@Override
public boolean supports(Class<?> authentication) {
return UsernamePasswordAuthenticationToken.class.equals(authentication);
}
}
UserDetailsService的作用是在实现AuthenticationProvider的authenticate()方法时,需要将前端的数据拿到该类的loadUserByUsername()中去判断数据库中去判断是否存在此人信息(判断过程注意注释)
@Service
public class CustomUserDetailsService implements UserDetailsService {
@Resource
PowerMapper powerMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 通过用户名从数据库获取用户信息(这里我就不粘贴了吧,就是基本的查询,建议角色表和用户表分开做,符合数据库的设计规则)
VipUser user = powerMapper.showPower(username);
// 角色集合
List<GrantedAuthority> authorities = new ArrayList<>();
if (user==null){
return null;
}
//将获取到的user.getRName()即角色权限放入authorities 中,最后作为其中一个参数返回
authorities.add(new SimpleGrantedAuthority(user.getRName()));
return new User(
user.getRName(),
user.getVPassword(),
authorities
);
}
}
本文你会发现对于在开头的WebSecurityConfigurerAdapter类中,所有的异常信息处理所实现的handler方法都写在链式编程里面的,会感觉很多,容易晕,所以建议你们分开写在不同的类里面,然后统一注入到WebSecurityConfigurerAdapter类中。
后期我也将会出一篇关于springboot2.x+springsecurity+redis+jwt+自定义验证码实现登录功能(登录功能为超时刷新和失效处理)
由于这个太多了,需要的私信我