package wf.spring; import cn.apiclub.captcha.Captcha; import cn.apiclub.captcha.backgrounds.FlatColorBackgroundProducer; import cn.apiclub.captcha.backgrounds.GradiatedBackgroundProducer; import cn.apiclub.captcha.gimpy.DropShadowGimpyRenderer; import cn.apiclub.captcha.gimpy.RippleGimpyRenderer; import cn.apiclub.captcha.text.renderer.ColoredEdgesWordRenderer; import cn.apiclub.captcha.text.renderer.DefaultWordRenderer; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.ObjectNode; import com.google.common.base.Objects; import com.google.common.base.Predicates; import com.google.common.collect.Iterables; import java.awt.Color; import java.awt.Font; import java.awt.Graphics2D; import java.awt.image.BufferedImage; import java.io.ByteArrayOutputStream; import java.security.SecureRandom; import java.util.ArrayList; import java.util.List; import java.util.Random; import javax.imageio.ImageIO; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.security.core.userdetails.User; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.server.ServerWebExchange; import org.springframework.web.server.WebSession; import reactor.core.publisher.Mono; import reactor.util.Logger; import wf.data.MemberManager; import wf.model.Member; import wf.model.WebSocketRedisListener; import wf.util.Loggers4j2; /** * * @author Kent Yeh */ @Controller public class RootController { private static final Logger logger = Loggers4j2.getLogger(RootController.class); public static final Random RAND = new SecureRandom(); private ObjectMapper objectMapper; private MemberManager memberManager; @Autowired public void setObjectMapper(ObjectMapper objectMapper) { this.objectMapper = objectMapper; } @Autowired public void setManager(MemberManager memberManager) { this.memberManager = memberManager; } //<editor-fold defaultstate="collapsed" desc="Grapic preparing"> private static final List<Color> COLORS = new ArrayList<>(2); private static final List<Font> FONTS = new ArrayList<>(3); private static final char[] DEFAULT_CHARS = {'A', 'B', 'C', 'D', 'e', 'E', 'F', 'g', 'H', 'K', 'k', 'L', 'M', 'm', 'N', 'n', '2', '3', '4', '5', '6', '7', '8', '9', 'N', 'P', 'p', 'R', 'S', 'T', 'U', 'V', 'W', 'w', 'X', 'x', 'Y', 'y', 'Z', 'z'}; static { COLORS.add(Color.BLACK); COLORS.add(Color.BLUE); COLORS.add(Color.CYAN); COLORS.add(Color.GREEN); COLORS.add(Color.MAGENTA); COLORS.add(Color.ORANGE); COLORS.add(Color.PINK); COLORS.add(Color.RED); COLORS.add(Color.YELLOW); FONTS.add(new Font("Times New Roman", Font.BOLD, 24)); FONTS.add(new Font("Times New Roman", Font.ITALIC, 24)); FONTS.add(new Font("Courier", Font.BOLD, 24)); FONTS.add(new Font("Courier", Font.ITALIC, 24)); FONTS.add(new Font("Monospace", Font.BOLD, 24)); FONTS.add(new Font("Monospace", Font.ITALIC, 24)); FONTS.add(new Font("Arial", Font.BOLD, 32)); FONTS.add(new Font("Arial", Font.ITALIC, 28)); } private String getChptcha(int len) { StringBuilder sb = new StringBuilder(); for (int i = 0; i < len; i++) { sb.append(DEFAULT_CHARS[RAND.nextInt(DEFAULT_CHARS.length)]); } return sb.toString(); } private Color getRandColor(int fc, int bc) { if (fc > 255) { fc = 255; } if (bc > 255) { bc = 255; } int r = fc + RAND.nextInt(bc - fc); int g = fc + RAND.nextInt(bc - fc); int b = fc + RAND.nextInt(bc - fc); return new Color(r, g, b); } //</editor-fold> @Value("#{systemProperties['captcha'] }") private String defaultCaptcha; @GetMapping({"/", "/index"}) public String index() { return "index"; } @GetMapping("/whoami") public String whomai(@AuthenticationPrincipal Mono<UserDetails> principal, Model model) { model.addAttribute("user", principal); return "index"; } /** * * @return PreAuthorize 回傳值必須是Mono/Flux 不可 */ @PreAuthorize("hasRole('ADMIN')") @GetMapping("/admin") public Mono<String> testAdmin() { return Mono.just("admin"); } @GetMapping("/static/captcha") public Mono<ResponseEntity<byte[]>> captcha(WebSession session, @RequestParam(name = "key", defaultValue = "captcha") String cpkey, @RequestParam(name = "len", defaultValue = "4") final int len) throws Exception { boolean rgr = RAND.nextBoolean(); Captcha captcha = new Captcha.Builder(80, 36) //<editor-fold defaultstate="collapsed" desc="draw"> .addNoise((BufferedImage bi) -> { Graphics2D g = bi.createGraphics(); int h = bi.getHeight(); int w = bi.getWidth(); for (int i = 0; i < 155; i++) { int x = RAND.nextInt(w); int y = RAND.nextInt(h); int xl = RAND.nextInt(10); int yl = RAND.nextInt(4); g.setColor(rgr ? getRandColor(32, 48) : getRandColor(0, 196)); g.drawLine(x, y, x + xl, y + yl); } }) .addText(() -> { //<editor-fold defaultstate="collapsed" desc="Random Text Generate"> String val = defaultCaptcha == null || defaultCaptcha.trim().isEmpty() ? getChptcha(len) : defaultCaptcha.trim(); session.getAttributes().put(cpkey, val); return val; //</editor-fold> }, RAND.nextBoolean() ? new ColoredEdgesWordRenderer(COLORS, FONTS) : new DefaultWordRenderer(COLORS, FONTS)) .gimp(rgr ? new RippleGimpyRenderer() : new DropShadowGimpyRenderer()) .addBorder() .addBackground(RAND.nextBoolean() ? new FlatColorBackgroundProducer() : new GradiatedBackgroundProducer()) //</editor-fold> .build(); try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) { ImageIO.write(captcha.getImage(), "png", baos); byte[] data = baos.toByteArray(); MultiValueMap<String, String> properties = new LinkedMultiValueMap<>(); properties.add("Cache-Control", "no-cache, no-store, max-age=0, must-revalidate"); properties.add("Content-Type", MediaType.IMAGE_PNG_VALUE);//如果用produces在舊版FF會有產生406錯誤 return Mono.just(new ResponseEntity<>(data, new HttpHeaders(properties), HttpStatus.OK)); } } @GetMapping("/login") public Mono<String> getLoginPage() { return Mono.just("login"); } @GetMapping("/hello/{member}") public String hello(@PathVariable("member") Member.Mono member, Model model) { model.addAttribute("user", member.get().switchIfEmpty(Mono.empty())); return "hello"; } @PostMapping("/putmyshtoppingcart") public Mono<ResponseEntity<String>> putAstaff(WebSession session, @AuthenticationPrincipal User principal) { List<WebSocketRedisListener> wsrls = FluxWebSocketHandler.userListenser.get(session.getId()); if (wsrls == null || wsrls.isEmpty()) { logger.error("{}沒有登錄資料", principal.getUsername()); return Mono.just(new ResponseEntity<>("沒有登錄資料", HttpStatus.FORBIDDEN)); } else { ObjectNode json = objectMapper.createObjectNode(); json.put("shoppingcard", "您剛把東西放進購物車"); JsonNode jn = objectMapper.convertValue(json, JsonNode.class); logger.info("publish {} {}", principal.getUsername(), jn.toString()); return wsrls.get(0).getReactiveRedisTemplate().convertAndSend(principal.getUsername(), jn) .then(Mono.just(new ResponseEntity<>("已放入購物車", HttpStatus.OK))); } } @GetMapping("/changePassword") public String getChangePassword() { return "changePassword"; } @PostMapping("/changePassword") public Mono<String> changePassword(@AuthenticationPrincipal UsernamePasswordAuthenticationToken principal, ServerWebExchange exchange, Model model) { return exchange.getFormData().doOnNext(mvm -> { if (Objects.equal(mvm.getFirst("oldPass"), mvm.getFirst("newPass"))) { model.addAttribute("message", "新舊密碼不可以相同"); } else if (!Objects.equal(mvm.getFirst("newPass"), mvm.getFirst("confirmPass"))) { model.addAttribute("message", "確認密碼與新密碼不符"); } }) .filter(mvm -> model.getAttribute("message") == null) .flatMap(mvm -> { String newPass = mvm.getFirst("newPass"); String oldPass = mvm.getFirst("oldPass"); return memberManager.changePassword(principal.getName(), newPass, oldPass) .doOnNext(i -> { model.addAttribute("message", i > 0 ? "密碼更新成功" : "更新密碼失敗"); }) .map(i -> i > 0 ? "index" : "changePassword"); }).switchIfEmpty(Mono.just("changePassword")); } @PreAuthorize("hasRole('ADMIN')") @GetMapping("/modifyMember/{member}") public Mono<String> memberAdminView(@PathVariable("member") Member.Mono member, Model model) { model.addAttribute("member", member.get()); return Mono.just("member"); } @PreAuthorize("hasRole('ADMIN')") @PostMapping("/modifyMember/{member}") public Mono<String> modifyMember(@PathVariable("member") Member.Mono oriMember, @ModelAttribute Member member, Model model) { //避免後面有值但第一個沒勾,導致null字串 Iterables.removeIf(member.getRoles(), Predicates.isNull()); model.addAttribute("member", memberManager.saveMember(member.setNew(false))); return Mono.just("member"); } @GetMapping("/ws") public String chat() { return "chat"; } }