pom.xml
- spring boot 版本:2.0.0.RELEASE
- 验证码:hutool工具生成,可以选择其他方式
- freemarker模板
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> <exclusions> <exclusion> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-tomcat</artifactId> </exclusion> </exclusions> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-freemarker</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <dependency> <groupId>com.xiaoleilu</groupId> <artifactId>hutool-core</artifactId> <version>3.3.2</version> </dependency>
|
application.yml
主要是freemarker配置
1 2 3 4 5 6 7 8 9 10 11
| spring: freemarker: cache: 'false' charset: UTF-8 check-template-location: 'true' content-type: text/html expose-request-attributes: 'true' expose-session-attributes: 'true' request-context-attribute: request suffix: .html template-loader-path: classpath:views
|
Security配置类
因为要用验证码验证,这里就不能用默认的formLogin()
了,需要添加登录认证过滤器CaptchaAuthenticationFilter
集成自
UsernamePasswordAuthenticationFilter
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50
| @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private CustomDetailService userDetailsService; @Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests() .antMatchers("/dwr/**","/user/getCaptcha","/tool/**","/toLogin","/logout","/**/favicon.ico").permitAll() .antMatchers("/**").hasRole("ADMIN") .anyRequest().fullyAuthenticated()
.and() .logout() .logoutUrl("/logout") .deleteCookies("remember-me") .logoutSuccessUrl("/toLogin") .permitAll() .and() .rememberMe(); http.headers().cacheControl(); http.headers().httpStrictTransportSecurity(); http.headers().xssProtection(); http.headers().contentTypeOptions(); CaptchaAuthenticationFilter captchaAuthenticationFilter = new CaptchaAuthenticationFilter(); captchaAuthenticationFilter.setAuthenticationManager(authenticationManager()); http.addFilterAfter(captchaAuthenticationFilter, BasicAuthenticationFilter.class) .exceptionHandling() .authenticationEntryPoint(new LoginUrlAuthenticationEntryPoint("/toLogin")); } @Override public void configure(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(userDetailsService) .passwordEncoder(new BCryptPasswordEncoder()); } @Override public void configure(WebSecurity web) throws Exception { web.ignoring().antMatchers("/res/**"); } }
|
- 在生成验证码的时候可以将验证码放入到session中
- 从session中取出验证码信息进行验证
LoginAuthenticationFailureHandler
,在验证码错误的时候能够抛出异常,重定向,定位错误信息
CaptchaException
定义了一个验证码异常,需要继承AuthenticationException
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
| public class CaptchaAuthenticationFilter extends UsernamePasswordAuthenticationFilter { public static final String SECURITY_FORM_CAPTCHA_KEY = "captcha"; public static final String SESSION_GENERATED_CAPTCHA_KEY = "circleCaptcha"; public CaptchaAuthenticationFilter() { this.setUsernameParameter("username"); this.setPasswordParameter("password"); this.setRequiresAuthenticationRequestMatcher(new AntPathRequestMatcher("/toLogin", "POST")); this.setAuthenticationFailureHandler(new LoginAuthenticationFailureHandler()); } @Override public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException { CircleCaptcha circleCaptcha = this.obtainGeneratedCaptcha(request); String inputCode = this.obtainCaptcha(request); if(!circleCaptcha.verify(inputCode)){ throw new CaptchaException("captcha not matched!"); } String username = obtainUsername(request); String password = obtainPassword(request);
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
setDetails(request, authRequest); return this.getAuthenticationManager().authenticate(authRequest); } protected String obtainCaptcha(HttpServletRequest request){ return request.getParameter(SECURITY_FORM_CAPTCHA_KEY); } protected CircleCaptcha obtainGeneratedCaptcha (HttpServletRequest request){ return (CircleCaptcha)request.getSession().getAttribute(SESSION_GENERATED_CAPTCHA_KEY); } }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| public class LoginAuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler { private static final String PASS_ERROR_URL = "/toLogin?error"; private static final String CAPTCHA_ERROR_URL = "/toLogin?captcha";
@Override public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
if (exception instanceof CaptchaException) { getRedirectStrategy().sendRedirect(request, response, CAPTCHA_ERROR_URL); } else { getRedirectStrategy().sendRedirect(request, response, PASS_ERROR_URL); } } }
|
1 2 3 4 5 6 7 8 9
| public class CaptchaException extends AuthenticationException {
private static final long serialVersionUID = 1L;
public CaptchaException(String msg) { super(msg); }
}
|
验证码
1 2 3 4 5 6 7 8 9 10 11
| @RequestMapping(value = "/getCaptcha", method = RequestMethod.GET) public void captcha(HttpServletRequest request, HttpServletResponse response) throws IOException{ CircleCaptcha captcha = CaptchaUtil.createCircleCaptcha(200, 100, 4, 20); HttpSession session = request.getSession(); session.setAttribute("circleCaptcha", captcha); response.setContentType("image/png"); OutputStream stream = response.getOutputStream(); captcha.write(stream); stream.flush(); stream.close(); }
|
UserDetailsService授权:从数据库中获取登录用户信息,权限,角色
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| @Service public class CustomDetailService implements UserDetailsService { @Autowired private SysUserDao userDao; @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { LoginUser user = userDao.findByName(username); if (user == null) { throw new UsernameNotFoundException(username); } return user; }
}
|
定义一个UserDetails类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94
| public class LoginUser implements UserDetails {
private static final long serialVersionUID = 4542856156463291156L;
private Integer id;
private String username;
private String password; private String headImg; private String role; private Date createTime; public Integer getId() { return id; }
public void setId(Integer id) { this.id = id; }
public void setPassword(String password) { this.password = password; }
public void setUsername(String username) { this.username = username; }
public String getRole() { return role; }
public void setRole(String role) { this.role = role; }
public String getHeadImg() { return headImg; }
public void setHeadImg(String headImg) { this.headImg = headImg; }
public Date getCreateTime() { return createTime; }
public void setCreateTime(Date createTime) { this.createTime = createTime; }
@Override public Collection<? extends GrantedAuthority> getAuthorities() { List<GrantedAuthority> auths = new ArrayList<>(); String role = this.getRole(); auths.add(new SimpleGrantedAuthority(role)); return auths; }
@Override public String getPassword() { return password; }
@Override public String getUsername() { return username; }
@Override public boolean isAccountNonExpired() { return true; }
@Override public boolean isAccountNonLocked() { return true; }
@Override public boolean isCredentialsNonExpired() { return true; }
@Override public boolean isEnabled() { return true; } }
|
登录跳转
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| @RequestMapping(value = "/toLogin", method = {RequestMethod.GET, RequestMethod.POST}) public ModelAndView getLoginPage(@RequestParam Optional<String> error, @RequestParam Optional<String> captcha, Map<String,Object> param) { if(error.isPresent()) { param.put("status", ErrorCode.loginFailed.getCode()); param.put("message","用户名或密码错误"); } else { param.put("status", ErrorCode.success.getCode()); param.put("message","登陆成功"); } if(captcha.isPresent()) { param.put("status", ErrorCode.loginFailed.getCode()); param.put("message","请输入正确的验证码"); } return new ModelAndView("user/login"); }
|
注意csrf
如果没有被禁用需要有${_csrf.token}
这个参数,remember-me
可以实现记住登录的功能需要在HttpSecurity
处开启
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| <form id="form" action="${base}/toLogin" method="post"> <div class="form-group has-feedback"> <input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}"/> <input type="email" class="form-control" name="username" placeholder="请输入用户名或邮箱"> <span class="glyphicon glyphicon-user form-control-feedback"></span> </div> <div class="form-group has-feedback"> <input type="password" class="form-control" name="password" placeholder="请输入密码"> <span class="glyphicon glyphicon-lock form-control-feedback"></span> </div> <div class="form-group"> <input type="text" class="form-control" name="captcha" style="width: 120px" placeholder="请输入验证码"> </div> <div class="form-group"> <img id="captcha" alt="" src="${base}/user/getCaptcha" height="50px" onclick="reImg()"> </div> <div class="row"> <div class="col-xs-12"> <div class="checkbox icheck"> <label> <input type="checkbox" name="remember-me" value="true" > <label for="ispersis">自动登陆</label> </label> </div> </div> <!-- /.col --> </div> </form>
|
退出表单
只支持post请求
1 2 3 4
| <form id="logoutForm" action="${base}/logout" method="post"> <input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}"/> <a href="#" class="btn bg-navy margin"><span class="glyphicon glyphicon-off"></span> 退出</a> </form>
|
页面要显示登录用户信息,需要定义request请求的全局变量,可以用拦截器,但有一种更简单的实现
1 2 3 4 5 6 7 8 9 10 11
| @ControllerAdvice public class CustomControllerAdvice { @ModelAttribute("user") public LoginUser getCurrentUser(Authentication authentication) { return (authentication == null) ? null : (LoginUser) authentication.getPrincipal(); } @ModelAttribute("base") public String getContextPath(HttpServletRequest request) { return request.getContextPath(); } }
|
v1.5.2